From 1170066a4dedd9b19539cbbea53ed067f95d02b2 Mon Sep 17 00:00:00 2001 From: Mihai Calin Luca Date: Mon, 23 Dec 2024 17:12:14 +0100 Subject: [PATCH 1/6] git scraper rust program used for creating training data --- Cargo.toml | 3 + tools/git-scraper/.gitignore | 1 + tools/git-scraper/Cargo.toml | 17 + tools/git-scraper/contracts_dump.txt | 8770 ++++++++++++++++++++++++++ tools/git-scraper/src/fetch.rs | 80 + tools/git-scraper/src/init.rs | 46 + tools/git-scraper/src/scraper.rs | 62 + tools/git-scraper/src/write.rs | 84 + 8 files changed, 9063 insertions(+) create mode 100644 tools/git-scraper/.gitignore create mode 100644 tools/git-scraper/Cargo.toml create mode 100644 tools/git-scraper/contracts_dump.txt create mode 100644 tools/git-scraper/src/fetch.rs create mode 100644 tools/git-scraper/src/init.rs create mode 100644 tools/git-scraper/src/scraper.rs create mode 100644 tools/git-scraper/src/write.rs diff --git a/Cargo.toml b/Cargo.toml index 3cc34e3bd4..766bf5fd6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,9 @@ members = [ "tools/payload-macro-generator", # "tools/plotter", "tools/interactor-system-func-calls/", + "tools/git-scraper/", + + "contracts/modules", diff --git a/tools/git-scraper/.gitignore b/tools/git-scraper/.gitignore new file mode 100644 index 0000000000..ab8b69cbc1 --- /dev/null +++ b/tools/git-scraper/.gitignore @@ -0,0 +1 @@ +config.toml \ No newline at end of file diff --git a/tools/git-scraper/Cargo.toml b/tools/git-scraper/Cargo.toml new file mode 100644 index 0000000000..fcef554ace --- /dev/null +++ b/tools/git-scraper/Cargo.toml @@ -0,0 +1,17 @@ +[[bin]] +name = "git-scraper" +path = "src/scraper.rs" + +[package] +name = "git-scraper" +version = "0.0.0" +publish = false +edition = "2021" +authors = ["you"] + +[dependencies] +reqwest = { version = "0.11", features = ["blocking", "json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +dotenv = "0.15" +toml = "0.8" diff --git a/tools/git-scraper/contracts_dump.txt b/tools/git-scraper/contracts_dump.txt new file mode 100644 index 0000000000..61afcfd4dd --- /dev/null +++ b/tools/git-scraper/contracts_dump.txt @@ -0,0 +1,8770 @@ +//////////////////////// +NAME: adder + +DESCRIPTION: +# Adder + +`Adder` is a simple Smart Contract. + + +SRC FOLDER: +FILE_NAME: adder.rs +#![no_std] + +use multiversx_sc::imports::*; + +pub mod adder_proxy; + +/// One of the simplest smart contracts possible, +/// it holds a single variable in storage, which anyone can increment. +#[multiversx_sc::contract] +pub trait Adder { + #[view(getSum)] + #[storage_mapper("sum")] + fn sum(&self) -> SingleValueMapper; + + #[init] + fn init(&self, initial_value: BigUint) { + self.sum().set(initial_value); + } + + #[upgrade] + fn upgrade(&self, initial_value: BigUint) { + self.init(initial_value); + } + + /// Add desired amount to the storage variable. + #[endpoint] + fn add(&self, value: BigUint) { + self.sum().update(|sum| *sum += value); + } +} + +FILE_NAME: adder_proxy.rs +// 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 AdderProxy; + +impl TxProxyTrait for AdderProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = AdderProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + AdderProxyMethods { wrapped_tx: tx } + } +} + +pub struct AdderProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl AdderProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + pub fn init< + Arg0: ProxyArg>, + >( + self, + initial_value: Arg0, + ) -> TxTypedDeploy { + self.wrapped_tx + .payment(NotPayable) + .raw_deploy() + .argument(&initial_value) + .original_result() + } +} + +#[rustfmt::skip] +impl AdderProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn upgrade< + Arg0: ProxyArg>, + >( + self, + initial_value: Arg0, + ) -> TxTypedUpgrade { + self.wrapped_tx + .payment(NotPayable) + .raw_upgrade() + .argument(&initial_value) + .original_result() + } +} + +#[rustfmt::skip] +impl AdderProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn sum( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getSum") + .original_result() + } + + /// Add desired amount to the storage variable. + pub fn add< + Arg0: ProxyArg>, + >( + self, + value: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("add") + .argument(&value) + .original_result() + } +} + + +CARGO.TOML: +[package] +name = "adder" +version = "0.0.0" +authors = ["Andrei Marinica "] +edition = "2021" +publish = false + +[lib] +path = "src/adder.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + +INTERACTOR FOLDER: None +//////////////////////// +NAME: bonding-curve-contract + +DESCRIPTION: +# Bonding Curve Contract + +This module enables using a bonding curve for defining the behaviour of the price of the token as its balance changes. + +Depositing a token will set 2 storages: + - `owned_tokens` which will store a list of `TokenIdentifier` of the tokens the seller deposits in the contract under the seller address as key and its pair + - `token_details` which will storea `TokenOwnershipData` object containing a list of the stored nonces and the address of the owner. + +This approach with 2 storages was importand because as seller I am interested only of what tokens I have available and for claiming everything they should be easy to find without me requiring to give arguments of what token I want to claim. From buyer point of view I am not interested of who is the owner, but at the same time the contract needs to make sure the payment goes to the right address. + +The payment was chosen to be stored under a storage named `BondingCurve` because here we have elements such as: + - `FunctionSelector` - an enum that contains the functions available for setting + - `CurveArguments` - containing: + - available_supply + - balance + - `TokenIdentifier` - containing the accepted payment token + - `Biguint` - containing the payment for the sold tokens + +Here the balance and the payment amount are variable and they will usually get changed together, reason why it was chosen for these elements to be kept away from `token_details`. + +**Important!** Only 1 seller can have a specific token to be sold at a time, avoiding this way scenarion of which token from which seller should be selled at one point. + +There is an option of `sell_availability` which can be set from the `init` of the contract allowing or denying a token once bought by a buyer to be sold back. + +The token availability can be checked via `getTokenAvailability` returning pairs of (`nonce`, `amount`) of the requested token. + +The buying function is constructed so that through the amount the buyer will receive the amount of tokens irrelevant of the nonces. If the buyer desires a specific token, he can provide in the optional parameter `requested_nonce` the nonce he desires and if it is available under the specified amount he will receive it. + +# Usage + +When using this contract one should do the following process for each deposited token: + - deposit the token + - set the curve function + - claim (when he wants to receive the payment and the unsold tokens) + +# Setting up the Curve Function + +For setting up the curve function the seller is requires to use the `setBondingCurve` endpoint providing a function for the seposited token. + +The bonding curve function configurations are set in the [function selector](docs/selector.md) +Here is where you would like to set your custom functions if the predefined ones are not what you are looking for. + +In the case where the curve function is not set, `FunctionSelector::None` will be the value of it, throwing an error until a proper function is set. + +SRC FOLDER: +FILE_NAME: bonding_curve_contract.rs +#![no_std] + +use multiversx_sc::imports::*; + +use function_selector::FunctionSelector; +use multiversx_sc_modules::{ + bonding_curve, + bonding_curve::utils::{events, owner_endpoints, storage, user_endpoints}, +}; +pub mod function_selector; + +#[multiversx_sc::contract] +pub trait Contract: + bonding_curve::BondingCurveModule + + storage::StorageModule + + events::EventsModule + + user_endpoints::UserEndpointsModule + + owner_endpoints::OwnerEndpointsModule +{ + #[init] + fn init(&self) {} + + #[payable("*")] + #[endpoint(sellToken)] + fn sell_token_endpoint(&self) { + self.sell_token::>(); + } + + #[payable("*")] + #[endpoint(buyToken)] + fn buy_token_endpoint( + &self, + requested_amount: BigUint, + requested_token: TokenIdentifier, + requested_nonce: OptionalValue, + ) { + self.buy_token::>( + requested_amount, + requested_token, + requested_nonce, + ); + } + + #[endpoint(deposit)] + #[payable("*")] + fn deposit_endpoint(&self, payment_token: OptionalValue) { + self.deposit::>(payment_token) + } + + #[endpoint(setBondingCurve)] + fn set_bonding_curve_endpoint( + &self, + identifier: TokenIdentifier, + function: FunctionSelector, + sell_availability: bool, + ) { + self.set_bonding_curve::>( + identifier, + function, + sell_availability, + ); + } + #[endpoint(claim)] + fn claim_endpoint(&self) { + self.claim::>(); + } + + #[view] + fn view_buy_price(&self, amount: BigUint, identifier: TokenIdentifier) -> BigUint { + self.get_buy_price::>(amount, identifier) + } + + #[view] + fn view_sell_price(&self, amount: BigUint, identifier: TokenIdentifier) -> BigUint { + self.get_sell_price::>(amount, identifier) + } +} + +FILE_NAME: function_selector.rs +use multiversx_sc::{derive_imports::*, imports::*}; + +use crate::bonding_curve::{ + curves::{curve_function::CurveFunction, linear_function::LinearFunction}, + utils::structs::CurveArguments, +}; + +#[type_abi] +#[derive(TopEncode, TopDecode, NestedEncode, NestedDecode, PartialEq, Eq, Clone, Default)] +pub enum FunctionSelector { + Linear(LinearFunction), + CustomExample(BigUint), + #[default] + None, +} + +impl CurveFunction for FunctionSelector { + fn calculate_price( + &self, + token_start: &BigUint, + amount: &BigUint, + arguments: &CurveArguments, + ) -> BigUint { + match &self { + FunctionSelector::Linear(linear_function) => { + linear_function.calculate_price(token_start, amount, arguments) + }, + + FunctionSelector::CustomExample(initial_cost) => { + let sum = token_start + amount; + &(&sum * &sum * sum / 3u32) + &arguments.balance + initial_cost.clone() + }, + FunctionSelector::None => { + M::error_api_impl().signal_error(b"Bonding Curve function is not assiged") + }, + } + } +} + + +CARGO.TOML: +[package] +name = "bonding-curve-contract" +version = "0.0.0" +authors = ["Alin Cruceat "] +edition = "2021" +publish = false + +[lib] +path = "src/bonding_curve_contract.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dependencies.multiversx-sc-modules] +version = "0.54.6" +path = "../../../contracts/modules" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: check-pause + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: check_pause.rs +#![no_std] + +use multiversx_sc::imports::*; + +use multiversx_sc_modules::pause; + +#[multiversx_sc::contract] +pub trait CheckPauseContract: pause::PauseModule { + #[init] + fn init(&self) {} + + #[endpoint(checkPause)] + fn check_pause(&self) -> bool { + self.is_paused() + } +} + + +CARGO.TOML: +[package] +name = "check-pause" +version = "0.0.0" +authors = ["Alin Cruceat "] +edition = "2021" +publish = false + +[lib] +path = "src/check_pause.rs" + +[dev-dependencies] +num-bigint = "0.4" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dependencies.multiversx-sc-modules] +version = "0.54.6" +path = "../../../contracts/modules" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: crowdfunding-esdt + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: crowdfunding_esdt.rs +#![no_std] + +use multiversx_sc::{derive_imports::*, imports::*}; +pub mod crowdfunding_esdt_proxy; + +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Eq, Clone, Copy, Debug)] +pub enum Status { + FundingPeriod, + Successful, + Failed, +} + +#[multiversx_sc::contract] +pub trait Crowdfunding { + #[init] + fn init(&self, target: BigUint, deadline: u64, token_identifier: EgldOrEsdtTokenIdentifier) { + require!(target > 0, "Target must be more than 0"); + self.target().set(target); + + require!( + deadline > self.get_current_time(), + "Deadline can't be in the past" + ); + self.deadline().set(deadline); + + require!(token_identifier.is_valid(), "Invalid token provided"); + self.cf_token_identifier().set(token_identifier); + } + + #[endpoint] + #[payable("*")] + fn fund(&self) { + let (token, _, payment) = self.call_value().egld_or_single_esdt().into_tuple(); + + require!( + self.status() == Status::FundingPeriod, + "cannot fund after deadline" + ); + require!(token == self.cf_token_identifier().get(), "wrong token"); + + let caller = self.blockchain().get_caller(); + self.deposit(&caller).update(|deposit| *deposit += payment); + } + + #[view] + fn status(&self) -> Status { + if self.get_current_time() < self.deadline().get() { + Status::FundingPeriod + } else if self.get_current_funds() >= self.target().get() { + Status::Successful + } else { + Status::Failed + } + } + + #[view(getCurrentFunds)] + #[title("currentFunds")] + fn get_current_funds(&self) -> BigUint { + let token = self.cf_token_identifier().get(); + + self.blockchain().get_sc_balance(&token, 0) + } + + #[endpoint] + fn claim(&self) { + match self.status() { + Status::FundingPeriod => sc_panic!("cannot claim before deadline"), + Status::Successful => { + let caller = self.blockchain().get_caller(); + require!( + caller == self.blockchain().get_owner_address(), + "only owner can claim successful funding" + ); + + let token_identifier = self.cf_token_identifier().get(); + let sc_balance = self.get_current_funds(); + + self.tx() + .to(&caller) + .egld_or_single_esdt(&token_identifier, 0, &sc_balance) + .transfer(); + }, + Status::Failed => { + let caller = self.blockchain().get_caller(); + let deposit = self.deposit(&caller).get(); + + if deposit > 0u32 { + let token_identifier = self.cf_token_identifier().get(); + + self.deposit(&caller).clear(); + self.tx() + .to(&caller) + .egld_or_single_esdt(&token_identifier, 0, &deposit) + .transfer(); + } + }, + } + } + + // private + + fn get_current_time(&self) -> u64 { + self.blockchain().get_block_timestamp() + } + + // storage + + #[view(getTarget)] + #[title("target")] + #[storage_mapper("target")] + fn target(&self) -> SingleValueMapper; + + #[view(getDeadline)] + #[title("deadline")] + #[storage_mapper("deadline")] + fn deadline(&self) -> SingleValueMapper; + + #[view(getDeposit)] + #[title("deposit")] + #[storage_mapper("deposit")] + fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper; + + #[view(getCrowdfundingTokenIdentifier)] + #[title("tokenIdentifier")] + #[storage_mapper("tokenIdentifier")] + fn cf_token_identifier(&self) -> SingleValueMapper; +} + +FILE_NAME: crowdfunding_esdt_proxy.rs +// 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 CrowdfundingProxy; + +impl TxProxyTrait for CrowdfundingProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = CrowdfundingProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + CrowdfundingProxyMethods { wrapped_tx: tx } + } +} + +pub struct CrowdfundingProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl CrowdfundingProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + pub fn init< + Arg0: ProxyArg>, + Arg1: ProxyArg, + Arg2: ProxyArg>, + >( + self, + target: Arg0, + deadline: Arg1, + token_identifier: Arg2, + ) -> TxTypedDeploy { + self.wrapped_tx + .payment(NotPayable) + .raw_deploy() + .argument(&target) + .argument(&deadline) + .argument(&token_identifier) + .original_result() + } +} + +#[rustfmt::skip] +impl CrowdfundingProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn fund( + self, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("fund") + .original_result() + } + + pub fn status( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("status") + .original_result() + } + + pub fn get_current_funds( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getCurrentFunds") + .original_result() + } + + pub fn claim( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("claim") + .original_result() + } + + pub fn target( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getTarget") + .original_result() + } + + pub fn deadline( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getDeadline") + .original_result() + } + + pub fn deposit< + Arg0: ProxyArg>, + >( + self, + donor: Arg0, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getDeposit") + .argument(&donor) + .original_result() + } + + pub fn cf_token_identifier( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getCrowdfundingTokenIdentifier") + .original_result() + } +} + +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Eq, Clone, Copy, Debug)] +pub enum Status { + FundingPeriod, + Successful, + Failed, +} + + +CARGO.TOML: +[package] +name = "crowdfunding-esdt" +version = "0.0.0" +authors = ["Dorin Iancu "] +edition = "2021" +publish = false + +[lib] +path = "src/crowdfunding_esdt.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + +[dev-dependencies] +num-bigint = "0.4" +num-traits = "0.2" +hex = "0.4" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: crypto-bubbles + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: crypto_bubbles.rs +#![no_std] + +use multiversx_sc::imports::*; + +#[multiversx_sc::contract] +pub trait CryptoBubbles { + /// constructor function + /// is called immediately after the contract is created + #[init] + fn init(&self) {} + + /// player adds funds + #[payable("EGLD")] + #[endpoint(topUp)] + fn top_up(&self) { + let payment = self.call_value().egld_value(); + let caller = self.blockchain().get_caller(); + self.player_balance(&caller) + .update(|balance| *balance += &*payment); + + self.top_up_event(&caller, &payment); + } + + /// player withdraws funds + #[endpoint] + fn withdraw(&self, amount: &BigUint) { + self.transfer_back_to_player_wallet(&self.blockchain().get_caller(), amount) + } + + /// server calls withdraw on behalf of the player + fn transfer_back_to_player_wallet(&self, player: &ManagedAddress, amount: &BigUint) { + self.player_balance(player).update(|balance| { + require!( + amount <= balance, + "amount to withdraw must be less or equal to balance" + ); + + *balance -= amount; + }); + + self.tx().to(player).egld(amount).transfer(); + + self.withdraw_event(player, amount); + } + + /// player joins game + fn add_player_to_game_state_change( + &self, + game_index: &BigUint, + player: &ManagedAddress, + bet: &BigUint, + ) { + self.player_balance(player).update(|balance| { + require!(bet <= balance, "insufficient funds to join game"); + + *balance -= bet; + }); + + self.player_joins_game_event(game_index, player, bet); + } + + // player tops up + joins a game + #[payable("EGLD")] + #[endpoint(joinGame)] + fn join_game(&self, game_index: BigUint) { + let bet = self.call_value().egld_value(); + let player = self.blockchain().get_caller(); + self.top_up(); + self.add_player_to_game_state_change(&game_index, &player, &bet) + } + + // owner transfers prize into winner SC account + #[only_owner] + #[endpoint(rewardWinner)] + fn reward_winner(&self, game_index: &BigUint, winner: &ManagedAddress, prize: &BigUint) { + self.player_balance(winner) + .update(|balance| *balance += prize); + + self.reward_winner_event(game_index, winner, prize); + } + + // owner transfers prize into winner SC account, then transfers funds to player wallet + #[endpoint(rewardAndSendToWallet)] + fn reward_and_send_to_wallet( + &self, + game_index: &BigUint, + winner: &ManagedAddress, + prize: &BigUint, + ) { + self.reward_winner(game_index, winner, prize); + self.transfer_back_to_player_wallet(winner, prize); + } + + // Storage + + #[view(balanceOf)] + #[storage_mapper("playerBalance")] + fn player_balance(&self, player: &ManagedAddress) -> SingleValueMapper; + + // Events + + #[event("top_up")] + fn top_up_event(&self, #[indexed] player: &ManagedAddress, amount: &BigUint); + + #[event("withdraw")] + fn withdraw_event(&self, #[indexed] player: &ManagedAddress, amount: &BigUint); + + #[event("player_joins_game")] + fn player_joins_game_event( + &self, + #[indexed] game_index: &BigUint, + #[indexed] player: &ManagedAddress, + bet: &BigUint, + ); + + #[event("reward_winner")] + fn reward_winner_event( + &self, + #[indexed] game_index: &BigUint, + #[indexed] winner: &ManagedAddress, + prize: &BigUint, + ); +} + + +CARGO.TOML: +[package] +name = "crypto-bubbles" +version = "0.0.0" +authors = ["Andrei Marinica "] +edition = "2021" +publish = false + +[lib] +path = "src/crypto_bubbles.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: crypto-kitties + +DESCRIPTION: +None + +SRC FOLDER: + +INTERACTOR FOLDER: None +//////////////////////// +NAME: crypto-zombies + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: kitty_obj.rs +use multiversx_sc::derive_imports::*; +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode)] +pub struct Kitty { + pub genes: KittyGenes, + pub birth_time: u64, // timestamp + pub cooldown_end: u64, // timestamp, used for pregnancy timer and siring cooldown + pub matron_id: u32, + pub sire_id: u32, + pub siring_with_id: u32, // for pregnant cats, 0 otherwise + pub nr_children: u16, // cooldown period increases exponentially with every breeding/siring + pub generation: u16, // max(sire_gen, matron_gen) + 1. Generation also influences cooldown. +} + +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode)] +pub struct KittyGenes { + pub fur_color: Color, + pub eye_color: Color, + pub meow_power: u8, // the higher the value, the louder the cat +} + +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, +} + +impl KittyGenes { + pub fn get_as_u64(&self) -> u64 { + (self.fur_color.as_u64() << 24 | self.eye_color.as_u64()) << 8 + | self.meow_power.to_be() as u64 + } +} + +impl Color { + pub fn as_u64(&self) -> u64 { + ((self.r.to_be() as u64) << 8 | self.r.to_be() as u64) << 8 | self.r.to_be() as u64 + } +} + +FILE_NAME: kitty_ownership_proxy.rs +// 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 KittyOwnershipProxy; + +impl TxProxyTrait for KittyOwnershipProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = KittyOwnershipProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + KittyOwnershipProxyMethods { wrapped_tx: tx } + } +} + +pub struct KittyOwnershipProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl KittyOwnershipProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + pub fn init< + Arg0: ProxyArg>, + Arg1: ProxyArg>>, + Arg2: ProxyArg>>, + >( + self, + birth_fee: Arg0, + opt_gene_science_contract_address: Arg1, + opt_kitty_auction_contract_address: Arg2, + ) -> TxTypedDeploy { + self.wrapped_tx + .payment(NotPayable) + .raw_deploy() + .argument(&birth_fee) + .argument(&opt_gene_science_contract_address) + .argument(&opt_kitty_auction_contract_address) + .original_result() + } +} + +#[rustfmt::skip] +impl KittyOwnershipProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn set_gene_science_contract_address_endpoint< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("setGeneScienceContractAddress") + .argument(&address) + .original_result() + } + + pub fn set_kitty_auction_contract_address_endpoint< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("setKittyAuctionContractAddress") + .argument(&address) + .original_result() + } + + pub fn claim( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("claim") + .original_result() + } + + pub fn total_supply( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("totalSupply") + .original_result() + } + + pub fn balance_of< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("balanceOf") + .argument(&address) + .original_result() + } + + pub fn owner_of< + Arg0: ProxyArg, + >( + self, + kitty_id: Arg0, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("ownerOf") + .argument(&kitty_id) + .original_result() + } + + pub fn approve< + Arg0: ProxyArg>, + Arg1: ProxyArg, + >( + self, + to: Arg0, + kitty_id: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("approve") + .argument(&to) + .argument(&kitty_id) + .original_result() + } + + pub fn transfer< + Arg0: ProxyArg>, + Arg1: ProxyArg, + >( + self, + to: Arg0, + kitty_id: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("transfer") + .argument(&to) + .argument(&kitty_id) + .original_result() + } + + pub fn transfer_from< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + Arg2: ProxyArg, + >( + self, + from: Arg0, + to: Arg1, + kitty_id: Arg2, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("transfer_from") + .argument(&from) + .argument(&to) + .argument(&kitty_id) + .original_result() + } + + pub fn tokens_of_owner< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("tokensOfOwner") + .argument(&address) + .original_result() + } + + pub fn allow_auctioning< + Arg0: ProxyArg>, + Arg1: ProxyArg, + >( + self, + by: Arg0, + kitty_id: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("allowAuctioning") + .argument(&by) + .argument(&kitty_id) + .original_result() + } + + pub fn approve_siring_and_return_kitty< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + Arg2: ProxyArg, + >( + self, + approved_address: Arg0, + kitty_owner: Arg1, + kitty_id: Arg2, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("approveSiringAndReturnKitty") + .argument(&approved_address) + .argument(&kitty_owner) + .argument(&kitty_id) + .original_result() + } + + pub fn create_gen_zero_kitty( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("createGenZeroKitty") + .original_result() + } + + pub fn get_kitty_by_id_endpoint< + Arg0: ProxyArg, + >( + self, + kitty_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getKittyById") + .argument(&kitty_id) + .original_result() + } + + pub fn is_ready_to_breed< + Arg0: ProxyArg, + >( + self, + kitty_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("isReadyToBreed") + .argument(&kitty_id) + .original_result() + } + + pub fn is_pregnant< + Arg0: ProxyArg, + >( + self, + kitty_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("isPregnant") + .argument(&kitty_id) + .original_result() + } + + pub fn can_breed_with< + Arg0: ProxyArg, + Arg1: ProxyArg, + >( + self, + matron_id: Arg0, + sire_id: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("canBreedWith") + .argument(&matron_id) + .argument(&sire_id) + .original_result() + } + + pub fn approve_siring< + Arg0: ProxyArg>, + Arg1: ProxyArg, + >( + self, + address: Arg0, + kitty_id: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("approveSiring") + .argument(&address) + .argument(&kitty_id) + .original_result() + } + + pub fn breed_with< + Arg0: ProxyArg, + Arg1: ProxyArg, + >( + self, + matron_id: Arg0, + sire_id: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("breedWith") + .argument(&matron_id) + .argument(&sire_id) + .original_result() + } + + pub fn give_birth< + Arg0: ProxyArg, + >( + self, + matron_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("giveBirth") + .argument(&matron_id) + .original_result() + } + + pub fn birth_fee( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("birthFee") + .original_result() + } +} + +FILE_NAME: lib.rs +#![no_std] + +use multiversx_sc::imports::*; + +pub mod kitty_obj; +pub mod kitty_ownership_proxy; +pub mod proxy_crypto_zombies; +mod storage; +mod zombie; +mod zombie_attack; +mod zombie_factory; +mod zombie_feeding; +mod zombie_helper; + +#[multiversx_sc::contract] +pub trait CryptoZombies: + zombie_factory::ZombieFactory + + zombie_feeding::ZombieFeeding + + storage::Storage + + zombie_helper::ZombieHelper + + zombie_attack::ZombieAttack +{ + #[init] + fn init(&self) { + self.dna_digits().set(16u8); + self.attack_victory_probability().set(70u8); + self.level_up_fee().set(BigUint::from(1000000000000000u64)); + self.cooldown_time().set(86400u64); + } + + #[upgrade] + fn upgrade(&self) {} + + #[only_owner] + #[endpoint] + fn set_crypto_kitties_sc_address(&self, address: ManagedAddress) { + self.crypto_kitties_sc_address().set(address); + } +} + +FILE_NAME: proxy_crypto_zombies.rs +// 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 CryptoZombiesProxy; + +impl TxProxyTrait for CryptoZombiesProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = CryptoZombiesProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + CryptoZombiesProxyMethods { wrapped_tx: tx } + } +} + +pub struct CryptoZombiesProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl CryptoZombiesProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + pub fn init( + self, + ) -> TxTypedDeploy { + self.wrapped_tx + .payment(NotPayable) + .raw_deploy() + .original_result() + } +} + +#[rustfmt::skip] +impl CryptoZombiesProxyMethods +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 CryptoZombiesProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn set_crypto_kitties_sc_address< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("set_crypto_kitties_sc_address") + .argument(&address) + .original_result() + } + + pub fn generate_random_dna( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("generate_random_dna") + .original_result() + } + + pub fn create_random_zombie< + Arg0: ProxyArg>, + >( + self, + name: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("create_random_zombie") + .argument(&name) + .original_result() + } + + pub fn is_ready< + Arg0: ProxyArg, + >( + self, + zombie_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("is_ready") + .argument(&zombie_id) + .original_result() + } + + pub fn feed_on_kitty< + Arg0: ProxyArg, + Arg1: ProxyArg, + >( + self, + zombie_id: Arg0, + kitty_id: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("feed_on_kitty") + .argument(&zombie_id) + .argument(&kitty_id) + .original_result() + } + + pub fn dna_digits( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("dna_digits") + .original_result() + } + + pub fn zombie_last_index( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("zombie_last_index") + .original_result() + } + + pub fn zombies< + Arg0: ProxyArg, + >( + self, + id: Arg0, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("zombies") + .argument(&id) + .original_result() + } + + pub fn zombie_owner< + Arg0: ProxyArg, + >( + self, + id: Arg0, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("zombie_owner") + .argument(&id) + .original_result() + } + + pub fn crypto_kitties_sc_address( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("crypto_kitties_sc_address") + .original_result() + } + + pub fn cooldown_time( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("cooldown_time") + .original_result() + } + + pub fn owned_zombies< + Arg0: ProxyArg>, + >( + self, + owner: Arg0, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("owned_zombies") + .argument(&owner) + .original_result() + } + + pub fn level_up< + Arg0: ProxyArg, + >( + self, + zombie_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("level_up") + .argument(&zombie_id) + .original_result() + } + + pub fn withdraw( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("withdraw") + .original_result() + } + + pub fn change_name< + Arg0: ProxyArg, + Arg1: ProxyArg>, + >( + self, + zombie_id: Arg0, + name: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("change_name") + .argument(&zombie_id) + .argument(&name) + .original_result() + } + + pub fn change_dna< + Arg0: ProxyArg, + Arg1: ProxyArg, + >( + self, + zombie_id: Arg0, + dna: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("change_dna") + .argument(&zombie_id) + .argument(&dna) + .original_result() + } + + pub fn attack< + Arg0: ProxyArg, + Arg1: ProxyArg, + >( + self, + zombie_id: Arg0, + target_id: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("attack") + .argument(&zombie_id) + .argument(&target_id) + .original_result() + } +} + +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode)] +pub struct Zombie +where + Api: ManagedTypeApi, +{ + pub name: ManagedBuffer, + pub dna: u64, + pub level: u16, + pub ready_time: u64, + pub win_count: usize, + pub loss_count: usize, +} + +FILE_NAME: storage.rs +use multiversx_sc::imports::*; + +use crate::zombie::Zombie; + +#[multiversx_sc::module] +pub trait Storage { + #[view] + #[storage_mapper("dnaDigits")] + fn dna_digits(&self) -> SingleValueMapper; + + #[view] + #[storage_mapper("zombieLastIndex")] + fn zombie_last_index(&self) -> SingleValueMapper; + + #[view] + #[storage_mapper("zombies")] + fn zombies(&self, id: &usize) -> SingleValueMapper>; + + #[view] + #[storage_mapper("zombieOwner")] + fn zombie_owner(&self, id: &usize) -> SingleValueMapper; + + #[view] + #[storage_mapper("cryptoKittiesScAddress")] + fn crypto_kitties_sc_address(&self) -> SingleValueMapper; + + #[view] + #[storage_mapper("cooldownTime")] + fn cooldown_time(&self) -> SingleValueMapper; + + #[view] + #[storage_mapper("ownedZombies")] + fn owned_zombies(&self, owner: &ManagedAddress) -> UnorderedSetMapper; + + #[storage_mapper("attackVictoryProbability")] + fn attack_victory_probability(&self) -> SingleValueMapper; + + #[storage_mapper("levelUpFee")] + fn level_up_fee(&self) -> SingleValueMapper; + + #[storage_mapper("collectedFees")] + fn collected_fees(&self) -> SingleValueMapper; +} + +FILE_NAME: zombie.rs +use multiversx_sc::{derive_imports::*, imports::*}; + +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode)] +pub struct Zombie { + pub name: ManagedBuffer, + pub dna: u64, + pub level: u16, + pub ready_time: u64, + pub win_count: usize, + pub loss_count: usize, +} + +FILE_NAME: zombie_attack.rs +use multiversx_sc::imports::*; + +use crate::{storage, zombie_factory, zombie_feeding, zombie_helper}; + +#[multiversx_sc::module] +pub trait ZombieAttack: + storage::Storage + + zombie_feeding::ZombieFeeding + + zombie_factory::ZombieFactory + + zombie_helper::ZombieHelper +{ + fn rand_mod(&self, modulus: u8) -> u8 { + let mut rand_source = RandomnessSource::new(); + rand_source.next_u8() % modulus + } + + #[endpoint] + fn attack(&self, zombie_id: usize, target_id: usize) { + let caller = self.blockchain().get_caller(); + self.check_zombie_belongs_to_caller(zombie_id, &caller); + let rand = self.rand_mod(100u8); + let attack_victory_probability = self.attack_victory_probability().get(); + if rand <= attack_victory_probability { + self.zombies(&zombie_id).update(|my_zombie| { + my_zombie.win_count += 1; + my_zombie.level += 1; + }); + + let mut enemy_dna = 0; + self.zombies(&target_id).update(|enemy_zombie| { + enemy_zombie.loss_count += 1; + enemy_dna = enemy_zombie.dna; + }); + self.feed_and_multiply(zombie_id, enemy_dna, ManagedBuffer::from(b"zombie")); + } else { + self.zombies(&zombie_id).update(|my_zombie| { + my_zombie.loss_count += 1; + }); + + self.zombies(&target_id).update(|enemy_zombie| { + enemy_zombie.win_count += 1; + }); + self.trigger_cooldown(zombie_id); + } + } +} + +FILE_NAME: zombie_factory.rs +use multiversx_sc::imports::*; + +use crate::{storage, zombie::Zombie}; + +#[multiversx_sc::module] +pub trait ZombieFactory: storage::Storage { + fn create_zombie(&self, owner: ManagedAddress, name: ManagedBuffer, dna: u64) { + self.zombie_last_index().update(|id| { + self.new_zombie_event(*id, &name, dna); + self.zombies(id).set(Zombie { + name, + dna, + level: 1u16, + ready_time: self.blockchain().get_block_timestamp(), + win_count: 0usize, + loss_count: 0usize, + }); + self.owned_zombies(&owner).insert(*id); + self.zombie_owner(id).set(owner); + *id += 1; + }); + } + + #[view] + fn generate_random_dna(&self) -> u64 { + let mut rand_source = RandomnessSource::new(); + let dna_digits = self.dna_digits().get(); + let max_dna_value = u64::pow(10u64, dna_digits as u32); + rand_source.next_u64_in_range(0u64, max_dna_value) + } + + #[endpoint] + fn create_random_zombie(&self, name: ManagedBuffer) { + let caller = self.blockchain().get_caller(); + require!( + self.owned_zombies(&caller).is_empty(), + "You already own a zombie" + ); + let rand_dna = self.generate_random_dna(); + self.create_zombie(caller, name, rand_dna); + } + + #[event("newZombieEvent")] + fn new_zombie_event( + &self, + #[indexed] zombie_id: usize, + name: &ManagedBuffer, + #[indexed] dna: u64, + ); +} + +FILE_NAME: zombie_feeding.rs +use multiversx_sc::imports::*; + +use crate::{kitty_obj::Kitty, kitty_ownership_proxy, storage, zombie_factory, zombie_helper}; + +#[multiversx_sc::module] +pub trait ZombieFeeding: + storage::Storage + zombie_factory::ZombieFactory + zombie_helper::ZombieHelper +{ + fn feed_and_multiply(&self, zombie_id: usize, target_dna: u64, species: ManagedBuffer) { + let caller = self.blockchain().get_caller(); + self.check_zombie_belongs_to_caller(zombie_id, &caller); + require!(self.is_ready(zombie_id), "Zombie is not ready"); + let my_zombie = self.zombies(&zombie_id).get(); + let dna_digits = self.dna_digits().get(); + let max_dna_value = u64::pow(10u64, dna_digits as u32); + let verified_target_dna = target_dna % max_dna_value; + let mut new_dna = (my_zombie.dna + verified_target_dna) / 2; + if species == b"kitty" { + new_dna = new_dna - new_dna % 100 + 99 + } + self.create_zombie(caller, ManagedBuffer::from(b"NoName"), new_dna); + self.trigger_cooldown(zombie_id); + } + + fn trigger_cooldown(&self, zombie_id: usize) { + let cooldown_time = self.cooldown_time().get(); + self.zombies(&zombie_id).update(|my_zombie| { + my_zombie.ready_time = self.blockchain().get_block_timestamp() + cooldown_time + }); + } + + #[view] + fn is_ready(&self, zombie_id: usize) -> bool { + let my_zombie = self.zombies(&zombie_id).get(); + my_zombie.ready_time <= self.blockchain().get_block_timestamp() + } + + #[callback] + fn get_kitty_callback( + &self, + #[call_result] result: ManagedAsyncCallResult, + zombie_id: usize, + ) { + match result { + ManagedAsyncCallResult::Ok(kitty) => { + let kitty_dna = kitty.genes.get_as_u64(); + self.feed_and_multiply(zombie_id, kitty_dna, ManagedBuffer::from(b"kitty")); + }, + ManagedAsyncCallResult::Err(_) => {}, + } + } + + #[endpoint] + fn feed_on_kitty(&self, zombie_id: usize, kitty_id: u32) { + let crypto_kitties_sc_address = self.crypto_kitties_sc_address().get(); + self.tx() + .to(&crypto_kitties_sc_address) + .typed(kitty_ownership_proxy::KittyOwnershipProxy) + .get_kitty_by_id_endpoint(kitty_id) + .callback(self.callbacks().get_kitty_callback(zombie_id)) + .async_call_and_exit(); + } +} + +FILE_NAME: zombie_helper.rs +use multiversx_sc::imports::*; + +use crate::storage; + +#[multiversx_sc::module] +pub trait ZombieHelper: storage::Storage { + fn check_above_level(&self, level: u16, zombie_id: usize) { + let my_zombie = self.zombies(&zombie_id).get(); + require!(my_zombie.level >= level, "Zombie is too low level"); + } + + fn check_zombie_belongs_to_caller(&self, zombie_id: usize, caller: &ManagedAddress) { + require!( + caller == &self.zombie_owner(&zombie_id).get(), + "Only the owner of the zombie can perform this operation" + ); + } + + #[payable("EGLD")] + #[endpoint] + fn level_up(&self, zombie_id: usize) { + let payment_amount = self.call_value().egld_value(); + let fee = self.level_up_fee().get(); + require!(*payment_amount == fee, "Payment must be must be 0.001 EGLD"); + self.zombies(&zombie_id) + .update(|my_zombie| my_zombie.level += 1); + } + + #[only_owner] + #[endpoint] + fn withdraw(&self) { + let caller_address = self.blockchain().get_caller(); + let collected_fees = self.collected_fees().get(); + self.tx() + .to(&caller_address) + .egld(&collected_fees) + .transfer(); + self.collected_fees().clear(); + } + + #[endpoint] + fn change_name(&self, zombie_id: usize, name: ManagedBuffer) { + self.check_above_level(2u16, zombie_id); + let caller = self.blockchain().get_caller(); + self.check_zombie_belongs_to_caller(zombie_id, &caller); + self.zombies(&zombie_id) + .update(|my_zombie| my_zombie.name = name); + } + + #[endpoint] + fn change_dna(&self, zombie_id: usize, dna: u64) { + self.check_above_level(20u16, zombie_id); + let caller = self.blockchain().get_caller(); + self.check_zombie_belongs_to_caller(zombie_id, &caller); + self.zombies(&zombie_id) + .update(|my_zombie| my_zombie.dna = dna); + } +} + + +CARGO.TOML: +[package] +name = "crypto-zombies" +version = "0.0.0" +authors = ["Alin Cruceat "] +edition = "2021" +publish = false + +[lib] +path = "src/lib.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: digital-cash + +DESCRIPTION: +# Digital Cash Contract + +The basic idea of MultiversX Digital Cash is that ONE link can hold information (ESDT tokens, EGLD) on chain, this link can be sent from one person to another, there is no need to hold any wallet to receive, consume and send it forward to another person. + +# Usage + +## Covering up payment fees & funding + +The contract allows funding any number of tokens in 1 call within a address under a valability if the fee cost was covered. + +In order to fund one should first call `deposit_fees` depositing the fee funds as `eGLD` within the contract. Only after, if the fees cover the transfer of the certain number of tokens, it is possible to deposit the funds, making them available for claiming or forwarding. + +`fund` after making sure everything is ok on the fee aspect will set up the `deposit` storage increasing the number of tokens to transfer by the number of tokens paid to the endpoint and set the expiration date by the number of rounds specified within the `valability` parameter. + +The fees are unique per address and only cover one instance of transfer, either if it is a `claim` or a `forward`, per number of tokens transfered. Only by making one of these actions will consume the fee funds following to to refund the rest of the fees to the depositor. + +## Claiming funds + +Claiming the funds require the signature parameter to be valid. Next the round will be checked to ve greater than the `expiration_round` within the deposit. Once these requirement was fulfilled the funds will be sent to the caller and the remaining of the fee funds sent back to the depositor. + +## Withdrawing funds + +If the valability of a deposit has expired it can no longer be claimed. Anyone on this point can call `withdraw` making the funds go back to the depositor together with the unused fee funds. + +## Forwarding funds + +Funds cam be forwarded to another address using the signature, but the forwarded address requires to have the fees covered. This actions will also consume the funds from the initial address. + +After the forward in case of a withdraw the funds will go to the `depositor_address` set within the `forwarded_address` deposit storage. + + +SRC FOLDER: +FILE_NAME: constants.rs +pub const NON_EXISTENT_KEY_ERR_MSG: &[u8] = b"non-existent key"; +pub const FEES_NOT_COVERED_ERR_MSG: &[u8] = b"fees not covered"; +pub const CANNOT_DEPOSIT_FUNDS_ERR_MSG: &[u8] = + b"cannot deposit funds without covering the fee cost first"; +pub const SECONDS_PER_ROUND: u64 = 6; + +FILE_NAME: deposit_info.rs +use multiversx_sc::{derive_imports::*, imports::*}; + +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode)] +pub struct DepositInfo { + pub depositor_address: ManagedAddress, + pub esdt_funds: ManagedVec>, + pub egld_funds: BigUint, + pub valability: u64, + pub expiration_round: u64, + pub fees: Fee, +} + +impl DepositInfo +where + M: ManagedTypeApi, +{ + pub fn get_num_tokens(&self) -> usize { + let mut amount = self.esdt_funds.len(); + if self.egld_funds > 0 { + amount += 1; + } + + amount + } +} + +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode)] +pub struct Fee { + pub num_token_to_transfer: usize, + pub value: EgldOrEsdtTokenPayment, +} + +FILE_NAME: digital_cash.rs +#![no_std] +#![allow(unused_attributes)] + +use multiversx_sc::imports::*; + +mod constants; +mod deposit_info; +pub mod digital_cash_proxy; +mod helpers; +mod pay_fee_and_fund; +mod signature_operations; +mod storage; + +use constants::*; + +#[multiversx_sc::contract] +pub trait DigitalCash: + pay_fee_and_fund::PayFeeAndFund + + signature_operations::SignatureOperationsModule + + helpers::HelpersModule + + storage::StorageModule +{ + #[init] + fn init(&self, fee: BigUint, token: EgldOrEsdtTokenIdentifier) { + self.whitelist_fee_token(fee, token); + } + + #[endpoint(whitelistFeeToken)] + #[only_owner] + fn whitelist_fee_token(&self, fee: BigUint, token: EgldOrEsdtTokenIdentifier) { + require!(self.fee(&token).is_empty(), "Token already whitelisted"); + self.fee(&token).set(fee); + self.whitelisted_fee_tokens().insert(token.clone()); + self.all_time_fee_tokens().insert(token); + } + + #[endpoint(blacklistFeeToken)] + #[only_owner] + fn blacklist_fee_token(&self, token: EgldOrEsdtTokenIdentifier) { + require!(!self.fee(&token).is_empty(), "Token is not whitelisted"); + self.fee(&token).clear(); + self.whitelisted_fee_tokens().swap_remove(&token); + } + + #[endpoint(claimFees)] + #[only_owner] + fn claim_fees(&self) { + let fee_tokens_mapper = self.all_time_fee_tokens(); + let fee_tokens = fee_tokens_mapper.iter(); + let caller_address = self.blockchain().get_caller(); + let mut collected_esdt_fees = ManagedVec::new(); + for token in fee_tokens { + let fee = self.collected_fees(&token).take(); + if fee == 0 { + continue; + } + if token == EgldOrEsdtTokenIdentifier::egld() { + self.tx().to(&caller_address).egld(&fee).transfer(); + } else { + let collected_fee = EsdtTokenPayment::new(token.unwrap_esdt(), 0, fee); + collected_esdt_fees.push(collected_fee); + } + } + if !collected_esdt_fees.is_empty() { + self.tx() + .to(&caller_address) + .payment(&collected_esdt_fees) + .transfer(); + } + } + + #[view(getAmount)] + fn get_amount( + &self, + address: ManagedAddress, + token: EgldOrEsdtTokenIdentifier, + nonce: u64, + ) -> BigUint { + let deposit_mapper = self.deposit(&address); + require!(!deposit_mapper.is_empty(), NON_EXISTENT_KEY_ERR_MSG); + + let deposit = deposit_mapper.get(); + if token.is_egld() { + return deposit.egld_funds; + } + + for esdt in deposit.esdt_funds.into_iter() { + if esdt.token_identifier == token && esdt.token_nonce == nonce { + return esdt.amount; + } + } + + BigUint::zero() + } +} + +FILE_NAME: digital_cash_proxy.rs +// 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 DigitalCashProxy; + +impl TxProxyTrait for DigitalCashProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = DigitalCashProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + DigitalCashProxyMethods { wrapped_tx: tx } + } +} + +pub struct DigitalCashProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl DigitalCashProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + pub fn init< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + >( + self, + fee: Arg0, + token: Arg1, + ) -> TxTypedDeploy { + self.wrapped_tx + .payment(NotPayable) + .raw_deploy() + .argument(&fee) + .argument(&token) + .original_result() + } +} + +#[rustfmt::skip] +impl DigitalCashProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn whitelist_fee_token< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + >( + self, + fee: Arg0, + token: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("whitelistFeeToken") + .argument(&fee) + .argument(&token) + .original_result() + } + + pub fn blacklist_fee_token< + Arg0: ProxyArg>, + >( + self, + token: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("blacklistFeeToken") + .argument(&token) + .original_result() + } + + pub fn claim_fees( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("claimFees") + .original_result() + } + + pub fn get_amount< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + Arg2: ProxyArg, + >( + self, + address: Arg0, + token: Arg1, + nonce: Arg2, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getAmount") + .argument(&address) + .argument(&token) + .argument(&nonce) + .original_result() + } + + pub fn pay_fee_and_fund_esdt< + Arg0: ProxyArg>, + Arg1: ProxyArg, + >( + self, + address: Arg0, + valability: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("payFeeAndFundESDT") + .argument(&address) + .argument(&valability) + .original_result() + } + + pub fn pay_fee_and_fund_egld< + Arg0: ProxyArg>, + Arg1: ProxyArg, + >( + self, + address: Arg0, + valability: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("payFeeAndFundEGLD") + .argument(&address) + .argument(&valability) + .original_result() + } + + pub fn fund< + Arg0: ProxyArg>, + Arg1: ProxyArg, + >( + self, + address: Arg0, + valability: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("fund") + .argument(&address) + .argument(&valability) + .original_result() + } + + pub fn deposit_fees< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("depositFees") + .argument(&address) + .original_result() + } + + pub fn withdraw< + Arg0: ProxyArg>, + >( + self, + address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("withdraw") + .argument(&address) + .original_result() + } + + pub fn claim< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + >( + self, + address: Arg0, + signature: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("claim") + .argument(&address) + .argument(&signature) + .original_result() + } + + pub fn forward< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + Arg2: ProxyArg>, + >( + self, + address: Arg0, + forward_address: Arg1, + signature: Arg2, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("forward") + .argument(&address) + .argument(&forward_address) + .argument(&signature) + .original_result() + } + + pub fn deposit< + Arg0: ProxyArg>, + >( + self, + donor: Arg0, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("deposit") + .argument(&donor) + .original_result() + } +} + +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode)] +pub struct DepositInfo +where + Api: ManagedTypeApi, +{ + pub depositor_address: ManagedAddress, + pub esdt_funds: ManagedVec>, + pub egld_funds: BigUint, + pub valability: u64, + pub expiration_round: u64, + pub fees: Fee, +} + +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode)] +pub struct Fee +where + Api: ManagedTypeApi, +{ + pub num_token_to_transfer: usize, + pub value: EgldOrEsdtTokenPayment, +} + +FILE_NAME: helpers.rs +use multiversx_sc::imports::*; + +use crate::{ + constants::*, + deposit_info::{DepositInfo, Fee}, + storage, +}; +#[multiversx_sc::module] +pub trait HelpersModule: storage::StorageModule { + fn send_fee_to_address(&self, fee: &EgldOrEsdtTokenPayment, address: &ManagedAddress) { + if fee.token_identifier == EgldOrEsdtTokenIdentifier::egld() { + self.tx().to(address).egld(&fee.amount).transfer(); + } else { + let esdt_fee = fee.clone().unwrap_esdt(); + self.tx() + .to(address) + .single_esdt(&esdt_fee.token_identifier, 0, &esdt_fee.amount) + .transfer(); + } + } + + fn get_num_token_transfers( + &self, + egld_value: &BigUint, + esdt_transfers: &ManagedVec, + ) -> usize { + let mut amount = esdt_transfers.len(); + if egld_value > &0 { + amount += 1; + } + + amount + } + + fn get_expiration_round(&self, valability: u64) -> u64 { + let valability_rounds = valability / SECONDS_PER_ROUND; + self.blockchain().get_block_round() + valability_rounds + } + + fn get_fee_for_token(&self, token: &EgldOrEsdtTokenIdentifier) -> BigUint { + require!( + self.whitelisted_fee_tokens().contains(token), + "invalid fee toke provided" + ); + let fee_token = self.fee(token); + fee_token.get() + } + + fn make_fund( + &self, + egld_payment: BigUint, + esdt_payment: ManagedVec, + address: ManagedAddress, + valability: u64, + ) { + let deposit_mapper = self.deposit(&address); + + deposit_mapper.update(|deposit| { + require!( + deposit.egld_funds == 0 && deposit.esdt_funds.is_empty(), + "key already used" + ); + let num_tokens = self.get_num_token_transfers(&egld_payment, &esdt_payment); + deposit.fees.num_token_to_transfer += num_tokens; + deposit.valability = valability; + deposit.expiration_round = self.get_expiration_round(valability); + deposit.esdt_funds = esdt_payment; + deposit.egld_funds = egld_payment; + }); + } + + fn check_fees_cover_number_of_tokens( + &self, + num_tokens: usize, + fee: BigUint, + paid_fee: BigUint, + ) { + require!(num_tokens > 0, "amount must be greater than 0"); + require!( + fee * num_tokens as u64 <= paid_fee, + CANNOT_DEPOSIT_FUNDS_ERR_MSG + ); + } + + fn update_fees( + &self, + caller_address: ManagedAddress, + address: &ManagedAddress, + payment: EgldOrEsdtTokenPayment, + ) { + self.get_fee_for_token(&payment.token_identifier); + let deposit_mapper = self.deposit(address); + if !deposit_mapper.is_empty() { + deposit_mapper.update(|deposit| { + require!( + deposit.depositor_address == caller_address, + "invalid depositor address" + ); + require!( + deposit.fees.value.token_identifier == payment.token_identifier, + "can only have 1 type of token as fee" + ); + deposit.fees.value.amount += payment.amount; + }); + return; + } + + let new_deposit = DepositInfo { + depositor_address: caller_address, + esdt_funds: ManagedVec::new(), + egld_funds: BigUint::zero(), + valability: 0, + expiration_round: 0, + fees: Fee { + num_token_to_transfer: 0, + value: payment, + }, + }; + deposit_mapper.set(new_deposit); + } +} + +FILE_NAME: pay_fee_and_fund.rs +use multiversx_sc::imports::*; + +use crate::{constants::*, helpers, storage}; + +#[multiversx_sc::module] +pub trait PayFeeAndFund: storage::StorageModule + helpers::HelpersModule { + #[endpoint(payFeeAndFundESDT)] + #[payable("*")] + fn pay_fee_and_fund_esdt(&self, address: ManagedAddress, valability: u64) { + let mut payments = self.call_value().all_esdt_transfers().clone_value(); + let fee = EgldOrEsdtTokenPayment::from(payments.get(0)); + let caller_address = self.blockchain().get_caller(); + self.update_fees(caller_address, &address, fee); + + payments.remove(0); + + self.make_fund(0u64.into(), payments, address, valability) + } + #[endpoint(payFeeAndFundEGLD)] + #[payable("EGLD")] + fn pay_fee_and_fund_egld(&self, address: ManagedAddress, valability: u64) { + let mut fund = self.call_value().egld_value().clone_value(); + let fee_value = self.fee(&EgldOrEsdtTokenIdentifier::egld()).get(); + require!(fund > fee_value, "payment not covering fees"); + + fund -= fee_value.clone(); + let fee = EgldOrEsdtTokenPayment::new(EgldOrEsdtTokenIdentifier::egld(), 0, fee_value); + let caller_address = self.blockchain().get_caller(); + self.update_fees(caller_address, &address, fee); + + self.make_fund(fund, ManagedVec::new(), address, valability); + } + + #[endpoint] + #[payable("*")] + fn fund(&self, address: ManagedAddress, valability: u64) { + require!(!self.deposit(&address).is_empty(), FEES_NOT_COVERED_ERR_MSG); + let deposit_mapper = self.deposit(&address).get(); + let depositor = deposit_mapper.depositor_address; + require!( + self.blockchain().get_caller() == depositor, + "invalid depositor" + ); + let deposited_fee_token = deposit_mapper.fees.value; + let fee_amount = self.fee(&deposited_fee_token.token_identifier).get(); + let egld_payment = self.call_value().egld_value().clone_value(); + let esdt_payment = self.call_value().all_esdt_transfers().clone_value(); + + let num_tokens = self.get_num_token_transfers(&egld_payment, &esdt_payment); + self.check_fees_cover_number_of_tokens(num_tokens, fee_amount, deposited_fee_token.amount); + + self.make_fund(egld_payment, esdt_payment, address, valability); + } + + #[endpoint(depositFees)] + #[payable("EGLD")] + fn deposit_fees(&self, address: &ManagedAddress) { + let payment = self.call_value().egld_or_single_esdt(); + let caller_address = self.blockchain().get_caller(); + self.update_fees(caller_address, address, payment); + } +} + +FILE_NAME: signature_operations.rs +use multiversx_sc::imports::*; + +use crate::{constants::*, helpers, storage}; + +pub use multiversx_sc::api::ED25519_SIGNATURE_BYTE_LEN; + +#[multiversx_sc::module] +pub trait SignatureOperationsModule: storage::StorageModule + helpers::HelpersModule { + #[endpoint] + fn withdraw(&self, address: ManagedAddress) { + let deposit_mapper = self.deposit(&address); + require!(!deposit_mapper.is_empty(), NON_EXISTENT_KEY_ERR_MSG); + + let deposit = deposit_mapper.take(); + let paid_fee_token = deposit.fees.value; + + let block_round = self.blockchain().get_block_round(); + require!( + deposit.expiration_round < block_round, + "withdrawal has not been available yet" + ); + + let mut egld_funds = deposit.egld_funds; + let mut esdt_funds = deposit.esdt_funds; + + if paid_fee_token.token_identifier == EgldOrEsdtTokenIdentifier::egld() { + egld_funds += paid_fee_token.amount; + } else { + let esdt_fee_token = paid_fee_token.unwrap_esdt(); + let esdt_fee = + EsdtTokenPayment::new(esdt_fee_token.token_identifier, 0, esdt_fee_token.amount); + esdt_funds.push(esdt_fee); + } + + if egld_funds > 0 { + self.tx() + .to(&deposit.depositor_address) + .egld(&egld_funds) + .transfer(); + } + + if !esdt_funds.is_empty() { + self.tx() + .to(&deposit.depositor_address) + .payment(esdt_funds) + .transfer(); + } + } + + #[endpoint] + fn claim( + &self, + address: ManagedAddress, + signature: ManagedByteArray, + ) { + let deposit_mapper = self.deposit(&address); + require!(!deposit_mapper.is_empty(), NON_EXISTENT_KEY_ERR_MSG); + + let caller_address = self.blockchain().get_caller(); + self.require_signature(&address, &caller_address, signature); + + let block_round = self.blockchain().get_block_round(); + let deposit = deposit_mapper.take(); + let num_tokens_transfered = deposit.get_num_tokens(); + let mut deposited_fee = deposit.fees.value; + + let fee_token = deposited_fee.token_identifier.clone(); + let fee = self.fee(&fee_token).get(); + require!(deposit.expiration_round >= block_round, "deposit expired"); + + let fee_cost = fee * num_tokens_transfered as u64; + deposited_fee.amount -= &fee_cost; + + self.collected_fees(&fee_token) + .update(|collected_fees| *collected_fees += fee_cost); + + if deposit.egld_funds > 0 { + self.tx() + .to(&caller_address) + .egld(&deposit.egld_funds) + .transfer(); + } + if !deposit.esdt_funds.is_empty() { + self.tx() + .to(&caller_address) + .payment(&deposit.esdt_funds) + .transfer(); + } + if deposited_fee.amount > 0 { + self.send_fee_to_address(&deposited_fee, &deposit.depositor_address); + } + } + + #[endpoint] + #[payable("*")] + fn forward( + &self, + address: ManagedAddress, + forward_address: ManagedAddress, + signature: ManagedByteArray, + ) { + let paid_fee = self.call_value().egld_or_single_esdt(); + let caller_address = self.blockchain().get_caller(); + let fee_token = paid_fee.token_identifier.clone(); + self.require_signature(&address, &caller_address, signature); + self.update_fees(caller_address, &forward_address, paid_fee); + + let new_deposit = self.deposit(&forward_address); + let fee = self.fee(&fee_token).get(); + + let mut current_deposit = self.deposit(&address).take(); + let num_tokens = current_deposit.get_num_tokens(); + new_deposit.update(|fwd_deposit| { + require!( + fwd_deposit.egld_funds == BigUint::zero() && fwd_deposit.esdt_funds.is_empty(), + "key already used" + ); + require!( + &fee * num_tokens as u64 <= fwd_deposit.fees.value.amount, + "cannot deposit funds without covering the fee cost first" + ); + + fwd_deposit.fees.num_token_to_transfer += num_tokens; + fwd_deposit.valability = current_deposit.valability; + fwd_deposit.expiration_round = self.get_expiration_round(current_deposit.valability); + fwd_deposit.esdt_funds = current_deposit.esdt_funds; + fwd_deposit.egld_funds = current_deposit.egld_funds; + }); + + let forward_fee = &fee * num_tokens as u64; + current_deposit.fees.value.amount -= &forward_fee; + + self.collected_fees(&fee_token) + .update(|collected_fees| *collected_fees += forward_fee); + + if current_deposit.fees.value.amount > 0 { + self.send_fee_to_address( + ¤t_deposit.fees.value, + ¤t_deposit.depositor_address, + ); + } + } + + fn require_signature( + &self, + address: &ManagedAddress, + caller_address: &ManagedAddress, + signature: ManagedByteArray, + ) { + let addr = address.as_managed_buffer(); + let message = caller_address.as_managed_buffer(); + self.crypto() + .verify_ed25519(addr, message, signature.as_managed_buffer()); + } +} + +FILE_NAME: storage.rs +use multiversx_sc::imports::*; + +use crate::deposit_info::*; + +#[multiversx_sc::module] +pub trait StorageModule { + #[view] + #[storage_mapper("deposit")] + fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper>; + + #[storage_mapper("fee")] + fn fee(&self, token: &EgldOrEsdtTokenIdentifier) -> SingleValueMapper; + + #[storage_mapper("collectedFees")] + fn collected_fees(&self, token: &EgldOrEsdtTokenIdentifier) -> SingleValueMapper; + + #[storage_mapper("whitelistedFeeTokens")] + fn whitelisted_fee_tokens(&self) -> UnorderedSetMapper; + + #[storage_mapper("allTimeFeeTokens")] + fn all_time_fee_tokens(&self) -> UnorderedSetMapper; +} + + +CARGO.TOML: +[package] +name = "digital-cash" +version = "0.0.0" +authors = [ "Valentin Craciun"] +edition = "2021" +publish = false + +[lib] +path = "src/digital_cash.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: empty + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: empty.rs +#![no_std] + +#[allow(unused_imports)] +use multiversx_sc::imports::*; + +/// An empty contract. To be used as a template when starting a new contract from scratch. +#[multiversx_sc::contract] +pub trait EmptyContract { + #[init] + fn init(&self) {} + + #[upgrade] + fn upgrade(&self) {} +} + + +CARGO.TOML: +[package] +name = "empty" +version = "0.0.0" +authors = ["you"] +edition = "2021" +publish = false + +[lib] +path = "src/empty.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + +[dev-dependencies] +num-bigint = "0.4" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: esdt-transfer-with-fee + +DESCRIPTION: +# Interaction + +The contract allows transfering token with the `ESDTRoleTransfer` role. + +The owner can set a fee for the contract, being one of 2 possible types: + +- `ExactValue` - `EsdtTokenPayment` type with desired token + amount per token transfered +- `Percentage` - % of the transfered token (this number is multiplied by 100 so that we can have 2 decimal percentages. ex.: 12,50% percentage fee will be set with 1250) + +The transfer endpoint requires the tokens having a `ExactValue` type fee to have the fee as the following token in exact amount. +The `Percentage` type will make the fee to be taken from the value transfered. + +Tokens that have no fee set will be simply transfered without additional requirements. + + +SRC FOLDER: +FILE_NAME: esdt_transfer_with_fee.rs +#![no_std] + +mod fee; +use fee::*; + +use multiversx_sc::imports::*; +#[multiversx_sc::contract] +pub trait EsdtTransferWithFee { + #[init] + fn init(&self) {} + + #[only_owner] + #[endpoint(setExactValueFee)] + fn set_exact_value_fee( + &self, + fee_token: TokenIdentifier, + fee_amount: BigUint, + token: TokenIdentifier, + ) { + self.token_fee(&token) + .set(Fee::ExactValue(EsdtTokenPayment::new( + fee_token, 0, fee_amount, + ))); + } + + #[only_owner] + #[endpoint(setPercentageFee)] + fn set_percentage_fee(&self, fee: u32, token: TokenIdentifier) { + self.token_fee(&token).set(Fee::Percentage(fee)); + } + + #[only_owner] + #[endpoint(claimFees)] + fn claim_fees(&self) { + let paid_fees = self.paid_fees(); + require!(!paid_fees.is_empty(), "There is nothing to claim"); + let mut fees = ManagedVec::new(); + for ((token, nonce), amount) in self.paid_fees().iter() { + fees.push(EsdtTokenPayment::new(token, nonce, amount)) + } + self.paid_fees().clear(); + + self.tx().to(ToCaller).payment(fees).transfer(); + } + + #[payable("*")] + #[endpoint] + fn transfer(&self, address: ManagedAddress) { + require!( + *self.call_value().egld_value() == 0, + "EGLD transfers not allowed" + ); + let payments = self.call_value().all_esdt_transfers(); + let mut new_payments = ManagedVec::new(); + + let mut payments_iter = payments.iter(); + while let Some(payment) = payments_iter.next() { + let fee_type = self.token_fee(&payment.token_identifier).get(); + match &fee_type { + Fee::ExactValue(fee) => { + let next_payment = payments_iter + .next() + .unwrap_or_else(|| sc_panic!("Fee payment missing")); + require!( + next_payment.token_identifier == fee.token_identifier + && next_payment.token_nonce == fee.token_nonce, + "Fee payment missing" + ); + require!( + next_payment.amount == fee.amount, + "Mismatching payment for covering fees" + ); + let _ = self.get_payment_after_fees(fee_type, &next_payment); + new_payments.push(payment); + }, + Fee::Percentage(_) => { + new_payments.push(self.get_payment_after_fees(fee_type, &payment)); + }, + Fee::Unset => { + new_payments.push(payment); + }, + } + } + self.tx().to(&address).payment(new_payments).transfer(); + } + + fn get_payment_after_fees( + &self, + fee: Fee, + payment: &EsdtTokenPayment, + ) -> EsdtTokenPayment { + let mut new_payment = payment.clone(); + let fee_payment = self.calculate_fee(&fee, payment.clone()); + + self.paid_fees() + .entry(( + new_payment.token_identifier.clone(), + new_payment.token_nonce, + )) + .or_insert(0u64.into()) + .update(|value| *value += &fee_payment.amount); + + new_payment.amount -= &fee_payment.amount; + new_payment + } + + fn calculate_fee( + &self, + fee: &Fee, + mut provided: EsdtTokenPayment, + ) -> EsdtTokenPayment { + match fee { + Fee::ExactValue(requested) => requested.clone(), + Fee::Percentage(percentage) => { + let calculated_fee_amount = &provided.amount * *percentage / PERCENTAGE_DIVISOR; + provided.amount = calculated_fee_amount; + provided + }, + Fee::Unset => { + provided.amount = BigUint::zero(); + provided + }, + } + } + + #[view(getTokenFee)] + #[storage_mapper("token_fee")] + fn token_fee(&self, token: &TokenIdentifier) -> SingleValueMapper>; + + #[view(getPaidFees)] + #[storage_mapper("paid_fees")] + fn paid_fees(&self) -> MapMapper<(TokenIdentifier, u64), BigUint>; +} + +FILE_NAME: fee.rs +use multiversx_sc::{derive_imports::*, imports::*}; + +pub(crate) const PERCENTAGE_DIVISOR: u32 = 10_000; // dividing the percentage fee by this number will result in a 2 decimal percentage + +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Eq, Clone)] +pub enum Fee +where + M: ManagedTypeApi, +{ + Unset, + ExactValue(EsdtTokenPayment), + Percentage(u32), +} + + +CARGO.TOML: +[package] +name = "esdt-transfer-with-fee" +version = "0.0.0" +authors = ["Alin Cruceat "] +edition = "2021" +publish = false + +[lib] +path = "src/esdt_transfer_with_fee.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: factorial + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: factorial.rs +#![no_std] +#![allow(unused_imports)] + +use multiversx_sc::imports::*; + +#[multiversx_sc::contract] +pub trait Factorial { + #[init] + fn init(&self) {} + + #[upgrade] + fn upgrade(&self) {} + + #[endpoint] + fn factorial(&self, value: BigUint) -> BigUint { + let one = BigUint::from(1u32); + if value == 0 { + return one; + } + + let mut result = BigUint::from(1u32); + let mut x = BigUint::from(1u32); + while x <= value { + result *= &x; + x += &one; + } + + result + } +} + + +CARGO.TOML: +[package] +name = "factorial" +version = "0.0.0" +authors = ["Andrei Marinica "] +edition = "2021" +publish = false + +[lib] +path = "src/factorial.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: fractional-nfts + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: fractional_nfts.rs +#![no_std] + +use multiversx_sc::imports::*; + +use multiversx_sc_modules::default_issue_callbacks; +mod fractional_uri_info; +use fractional_uri_info::FractionalUriInfo; +pub mod nft_marketplace_proxy; + +#[multiversx_sc::contract] +pub trait FractionalNfts: default_issue_callbacks::DefaultIssueCallbacksModule { + #[init] + fn init(&self) {} + + #[only_owner] + #[payable("EGLD")] + fn issue_and_set_all_roles( + &self, + token_display_name: ManagedBuffer, + token_ticker: ManagedBuffer, + num_decimals: usize, + ) { + let issue_cost = self.call_value().egld_value(); + self.fractional_token().issue_and_set_all_roles( + EsdtTokenType::SemiFungible, + issue_cost.clone_value(), + token_display_name, + token_ticker, + num_decimals, + None, + ); + } + + #[only_owner] + #[endpoint(claimRoyaltiesFromMarketplace)] + fn claim_royalties_from_marketplace( + &self, + marketplace_address: ManagedAddress, + token_id: TokenIdentifier, + token_nonce: u64, + ) { + let caller = self.blockchain().get_caller(); + self.tx() + .to(&marketplace_address) + .typed(nft_marketplace_proxy::NftMarketplaceProxy) + .claim_tokens(caller, token_id, token_nonce) + .async_call_and_exit(); + } + + #[payable("*")] + #[endpoint(fractionalizeNFT)] + fn fractionalize_nft( + &self, + initial_fractional_amount: BigUint, + name: ManagedBuffer, + attributes: ManagedBuffer, + ) { + let original_payment = self.call_value().single_esdt(); + let sc_address = self.blockchain().get_sc_address(); + let original_token_data = self.blockchain().get_esdt_token_data( + &sc_address, + &original_payment.token_identifier, + original_payment.token_nonce, + ); + + let sc_owner = self.blockchain().get_owner_address(); + require!( + original_token_data.creator == sc_address || original_token_data.creator == sc_owner, + "Wrong payment creator" + ); + + let fractional_token_mapper = self.fractional_token(); + fractional_token_mapper.require_issued_or_set(); + let fractional_token = fractional_token_mapper.get_token_id_ref(); + let hash = ManagedBuffer::new(); + let fractional_info = + FractionalUriInfo::new(original_payment, initial_fractional_amount.clone()); + let uris = fractional_info.to_uris(); + + let fractional_nonce = self.send().esdt_nft_create( + fractional_token, + &initial_fractional_amount, + &name, + &original_token_data.royalties, + &hash, + &attributes, + &uris, + ); + + self.tx() + .to(ToCaller) + .single_esdt( + fractional_token, + fractional_nonce, + &initial_fractional_amount, + ) + .transfer(); + } + + #[payable("*")] + #[endpoint(unFractionalizeNFT)] + fn unfractionalize_nft(&self) { + let fractional_payment = self.call_value().single_esdt(); + let fractional_token_mapper = self.fractional_token(); + + fractional_token_mapper.require_issued_or_set(); + fractional_token_mapper.require_same_token(&fractional_payment.token_identifier); + + let sc_address = self.blockchain().get_sc_address(); + let token_data = self.blockchain().get_esdt_token_data( + &sc_address, + &fractional_payment.token_identifier, + fractional_payment.token_nonce, + ); + + let fractional_info = FractionalUriInfo::from_uris(token_data.uris); + require!( + fractional_info.initial_fractional_amount == fractional_payment.amount, + "Must provide the full initial amount" + ); + + self.send().esdt_local_burn( + &fractional_payment.token_identifier, + fractional_payment.token_nonce, + &fractional_payment.amount, + ); + + let original = fractional_info.original_payment; + + self.tx() + .to(ToCaller) + .single_esdt( + &original.token_identifier, + original.token_nonce, + &original.amount, + ) + .transfer(); + } + + #[view(getFractionalToken)] + #[storage_mapper("fractional_token")] + fn fractional_token(&self) -> NonFungibleTokenMapper; +} + +FILE_NAME: fractional_uri_info.rs +use multiversx_sc::{derive_imports::*, imports::*}; + +use multiversx_sc::contract_base::ManagedSerializer; + +#[derive(TopEncode, TopDecode)] +pub struct FractionalUriInfo { + pub original_payment: EsdtTokenPayment, + pub initial_fractional_amount: BigUint, +} + +impl FractionalUriInfo { + pub fn new( + original_payment: EsdtTokenPayment, + initial_fractional_amount: BigUint, + ) -> Self { + Self { + original_payment, + initial_fractional_amount, + } + } + + pub fn from_uris(uris: ManagedVec>) -> Self { + let first_uri = uris + .try_get(0) + .unwrap_or_else(|| M::error_api_impl().signal_error(b"No URIs in fractional token")); + let serializer = ManagedSerializer::new(); + serializer.top_decode_from_managed_buffer_custom_message( + &first_uri, + "Invalid Fractional URI info", + ) + } + + pub fn to_uris(&self) -> ManagedVec> { + let first_uri = ManagedSerializer::new().top_encode_to_managed_buffer(&self); + ManagedVec::from_single_item(first_uri) + } +} + +FILE_NAME: nft_marketplace_proxy.rs +#![allow(clippy::all)] + +use multiversx_sc::proxy_imports::*; + +pub struct NftMarketplaceProxy; + +impl TxProxyTrait for NftMarketplaceProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = NftMarketplaceProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + NftMarketplaceProxyMethods { wrapped_tx: tx } + } +} + +pub struct NftMarketplaceProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl NftMarketplaceProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn claim_tokens< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + Arg2: ProxyArg, + >( + self, + claim_destination: Arg0, + token_id: Arg1, + token_nonce: Arg2, + ) -> TxTypedCall, ManagedVec>>> { + self.wrapped_tx + .raw_call("claimTokens") + .argument(&claim_destination) + .argument(&token_id) + .argument(&token_nonce) + .original_result() + } +} + + +CARGO.TOML: +[package] +name = "fractional-nfts" +version = "0.0.0" +authors = ["Claudiu-Marcel Bruda "] +edition = "2021" +publish = false + +[lib] +path = "src/fractional_nfts.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dependencies.multiversx-sc-modules] +version = "0.54.6" +path = "../../../contracts/modules" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: lottery-esdt + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: lottery.rs +#![no_std] + +use multiversx_sc::imports::*; + +mod lottery_info; +mod status; + +use lottery_info::LotteryInfo; +use status::Status; + +const PERCENTAGE_TOTAL: u32 = 100; +const THIRTY_DAYS_IN_SECONDS: u64 = 60 * 60 * 24 * 30; +const MAX_TICKETS: usize = 800; + +#[multiversx_sc::contract] +pub trait Lottery { + #[init] + fn init(&self) {} + + #[allow_multiple_var_args] + #[endpoint] + fn start( + &self, + lottery_name: ManagedBuffer, + token_identifier: EgldOrEsdtTokenIdentifier, + ticket_price: BigUint, + opt_total_tickets: Option, + opt_deadline: Option, + opt_max_entries_per_user: Option, + opt_prize_distribution: ManagedOption>, + opt_whitelist: ManagedOption>, + opt_burn_percentage: OptionalValue, + ) { + self.start_lottery( + lottery_name, + token_identifier, + ticket_price, + opt_total_tickets, + opt_deadline, + opt_max_entries_per_user, + opt_prize_distribution, + opt_whitelist, + opt_burn_percentage, + ); + } + + #[allow_multiple_var_args] + #[endpoint(createLotteryPool)] + fn create_lottery_pool( + &self, + lottery_name: ManagedBuffer, + token_identifier: EgldOrEsdtTokenIdentifier, + ticket_price: BigUint, + opt_total_tickets: Option, + opt_deadline: Option, + opt_max_entries_per_user: Option, + opt_prize_distribution: ManagedOption>, + opt_whitelist: ManagedOption>, + opt_burn_percentage: OptionalValue, + ) { + self.start_lottery( + lottery_name, + token_identifier, + ticket_price, + opt_total_tickets, + opt_deadline, + opt_max_entries_per_user, + opt_prize_distribution, + opt_whitelist, + opt_burn_percentage, + ); + } + + #[allow_multiple_var_args] + #[allow(clippy::too_many_arguments)] + fn start_lottery( + &self, + lottery_name: ManagedBuffer, + token_identifier: EgldOrEsdtTokenIdentifier, + ticket_price: BigUint, + opt_total_tickets: Option, + opt_deadline: Option, + opt_max_entries_per_user: Option, + opt_prize_distribution: ManagedOption>, + opt_whitelist: ManagedOption>, + opt_burn_percentage: OptionalValue, + ) { + require!(!lottery_name.is_empty(), "Name can't be empty!"); + + let timestamp = self.blockchain().get_block_timestamp(); + let total_tickets = opt_total_tickets.unwrap_or(MAX_TICKETS); + let deadline = opt_deadline.unwrap_or(timestamp + THIRTY_DAYS_IN_SECONDS); + let max_entries_per_user = opt_max_entries_per_user.unwrap_or(MAX_TICKETS); + let prize_distribution = opt_prize_distribution + .unwrap_or_else(|| ManagedVec::from_single_item(PERCENTAGE_TOTAL as u8)); + + require!( + self.status(&lottery_name) == Status::Inactive, + "Lottery is already active!" + ); + require!(!lottery_name.is_empty(), "Can't have empty lottery name!"); + require!(token_identifier.is_valid(), "Invalid token name provided!"); + require!(ticket_price > 0, "Ticket price must be higher than 0!"); + require!( + total_tickets > 0, + "Must have more than 0 tickets available!" + ); + require!( + total_tickets <= MAX_TICKETS, + "Only 800 or less total tickets per lottery are allowed!" + ); + require!(deadline > timestamp, "Deadline can't be in the past!"); + require!( + deadline <= timestamp + THIRTY_DAYS_IN_SECONDS, + "Deadline can't be later than 30 days from now!" + ); + require!( + max_entries_per_user > 0, + "Must have more than 0 max entries per user!" + ); + require!( + self.sum_array(&prize_distribution) == PERCENTAGE_TOTAL, + "Prize distribution must add up to exactly 100(%)!" + ); + + match opt_burn_percentage { + OptionalValue::Some(burn_percentage) => { + require!(!token_identifier.is_egld(), "EGLD can't be burned!"); + + let roles = self + .blockchain() + .get_esdt_local_roles(&token_identifier.clone().unwrap_esdt()); + require!( + roles.has_role(&EsdtLocalRole::Burn), + "The contract can't burn the selected token!" + ); + + require!( + burn_percentage < PERCENTAGE_TOTAL, + "Invalid burn percentage!" + ); + self.burn_percentage_for_lottery(&lottery_name) + .set(burn_percentage); + }, + OptionalValue::None => {}, + } + + if let Some(whitelist) = opt_whitelist.as_option() { + let mut mapper = self.lottery_whitelist(&lottery_name); + for addr in &*whitelist { + mapper.insert(addr); + } + } + + let info = LotteryInfo { + token_identifier, + ticket_price, + tickets_left: total_tickets, + deadline, + max_entries_per_user, + prize_distribution, + prize_pool: BigUint::zero(), + }; + + self.lottery_info(&lottery_name).set(&info); + } + + #[endpoint] + #[payable("*")] + fn buy_ticket(&self, lottery_name: ManagedBuffer) { + let (token_identifier, payment) = self.call_value().egld_or_single_fungible_esdt(); + + match self.status(&lottery_name) { + Status::Inactive => sc_panic!("Lottery is currently inactive."), + Status::Running => { + self.update_after_buy_ticket(&lottery_name, &token_identifier, &payment) + }, + Status::Ended => { + sc_panic!("Lottery entry period has ended! Awaiting winner announcement.") + }, + }; + } + + #[endpoint] + fn determine_winner(&self, lottery_name: ManagedBuffer) { + match self.status(&lottery_name) { + Status::Inactive => sc_panic!("Lottery is inactive!"), + Status::Running => sc_panic!("Lottery is still running!"), + Status::Ended => { + self.distribute_prizes(&lottery_name); + self.clear_storage(&lottery_name); + }, + }; + } + + #[view] + fn status(&self, lottery_name: &ManagedBuffer) -> Status { + if self.lottery_info(lottery_name).is_empty() { + return Status::Inactive; + } + + let info = self.lottery_info(lottery_name).get(); + let current_time = self.blockchain().get_block_timestamp(); + if current_time > info.deadline || info.tickets_left == 0 { + return Status::Ended; + } + + Status::Running + } + + fn update_after_buy_ticket( + &self, + lottery_name: &ManagedBuffer, + token_identifier: &EgldOrEsdtTokenIdentifier, + payment: &BigUint, + ) { + let info_mapper = self.lottery_info(lottery_name); + let mut info = info_mapper.get(); + let caller = self.blockchain().get_caller(); + let whitelist = self.lottery_whitelist(lottery_name); + + require!( + whitelist.is_empty() || whitelist.contains(&caller), + "You are not allowed to participate in this lottery!" + ); + require!( + token_identifier == &info.token_identifier && payment == &info.ticket_price, + "Wrong ticket fee!" + ); + + let entries_mapper = self.number_of_entries_for_user(lottery_name, &caller); + let mut entries = entries_mapper.get(); + require!( + entries < info.max_entries_per_user, + "Ticket limit exceeded for this lottery!" + ); + + self.ticket_holders(lottery_name).push(&caller); + + entries += 1; + info.tickets_left -= 1; + info.prize_pool += &info.ticket_price; + + entries_mapper.set(entries); + info_mapper.set(&info); + } + + fn distribute_prizes(&self, lottery_name: &ManagedBuffer) { + let mut info = self.lottery_info(lottery_name).get(); + let ticket_holders_mapper = self.ticket_holders(lottery_name); + let total_tickets = ticket_holders_mapper.len(); + + if total_tickets == 0 { + return; + } + + let burn_percentage = self.burn_percentage_for_lottery(lottery_name).get(); + if burn_percentage > 0 { + let burn_amount = self.calculate_percentage_of(&info.prize_pool, &burn_percentage); + + // Prevent crashing if the role was unset while the lottery was running + // The tokens will simply remain locked forever + let esdt_token_id = info.token_identifier.clone().unwrap_esdt(); + let roles = self.blockchain().get_esdt_local_roles(&esdt_token_id); + if roles.has_role(&EsdtLocalRole::Burn) { + self.send().esdt_local_burn(&esdt_token_id, 0, &burn_amount); + } + + info.prize_pool -= burn_amount; + } + + // 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() { + total_tickets + } else { + info.prize_distribution.len() + }; + let total_prize = info.prize_pool.clone(); + let winning_tickets = self.get_distinct_random(1, total_tickets, total_winning_tickets); + + // 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. + for i in (1..total_winning_tickets).rev() { + let winning_ticket_id = winning_tickets[i]; + let winner_address = ticket_holders_mapper.get(winning_ticket_id); + let prize = self.calculate_percentage_of( + &total_prize, + &BigUint::from(info.prize_distribution.get(i)), + ); + + self.tx() + .to(&winner_address) + .egld_or_single_esdt(&info.token_identifier, 0, &prize) + .transfer(); + info.prize_pool -= prize; + } + + // send leftover to first place + let first_place_winner = ticket_holders_mapper.get(winning_tickets[0]); + self.tx() + .to(&first_place_winner) + .egld_or_single_esdt(&info.token_identifier, 0, &info.prize_pool) + .transfer(); + } + + fn clear_storage(&self, lottery_name: &ManagedBuffer) { + let mut ticket_holders_mapper = self.ticket_holders(lottery_name); + let current_ticket_number = ticket_holders_mapper.len(); + + for i in 1..=current_ticket_number { + let addr = ticket_holders_mapper.get(i); + self.number_of_entries_for_user(lottery_name, &addr).clear(); + } + + ticket_holders_mapper.clear(); + self.lottery_info(lottery_name).clear(); + self.lottery_whitelist(lottery_name).clear(); + self.burn_percentage_for_lottery(lottery_name).clear(); + } + + fn sum_array(&self, array: &ManagedVec) -> u32 { + let mut sum = 0; + + for item in array { + sum += item as u32; + } + + sum + } + + /// does not check if max - min >= amount, that is the caller's job + fn get_distinct_random( + &self, + min: usize, + max: usize, + amount: usize, + ) -> ArrayVec { + let mut rand_numbers = ArrayVec::new(); + + for num in min..=max { + rand_numbers.push(num); + } + + let total_numbers = rand_numbers.len(); + let mut rand = RandomnessSource::new(); + + for i in 0..amount { + let rand_index = rand.next_usize_in_range(0, total_numbers); + rand_numbers.swap(i, rand_index); + } + + rand_numbers + } + + fn calculate_percentage_of(&self, value: &BigUint, percentage: &BigUint) -> BigUint { + value * percentage / PERCENTAGE_TOTAL + } + + // storage + + #[view(getLotteryInfo)] + #[storage_mapper("lotteryInfo")] + fn lottery_info( + &self, + lottery_name: &ManagedBuffer, + ) -> SingleValueMapper>; + + #[view(getLotteryWhitelist)] + #[storage_mapper("lotteryWhitelist")] + fn lottery_whitelist(&self, lottery_name: &ManagedBuffer) + -> UnorderedSetMapper; + + #[storage_mapper("ticketHolder")] + fn ticket_holders(&self, lottery_name: &ManagedBuffer) -> VecMapper; + + #[storage_mapper("numberOfEntriesForUser")] + fn number_of_entries_for_user( + &self, + lottery_name: &ManagedBuffer, + user: &ManagedAddress, + ) -> SingleValueMapper; + + #[storage_mapper("burnPercentageForLottery")] + fn burn_percentage_for_lottery( + &self, + lottery_name: &ManagedBuffer, + ) -> SingleValueMapper; +} + +FILE_NAME: lottery_info.rs +use multiversx_sc::{ + api::ManagedTypeApi, + types::{BigUint, EgldOrEsdtTokenIdentifier, ManagedVec}, +}; + +use multiversx_sc::derive_imports::*; + +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode)] +pub struct LotteryInfo { + pub token_identifier: EgldOrEsdtTokenIdentifier, + pub ticket_price: BigUint, + pub tickets_left: usize, + pub deadline: u64, + pub max_entries_per_user: usize, + pub prize_distribution: ManagedVec, + pub prize_pool: BigUint, +} + +FILE_NAME: status.rs +use multiversx_sc::derive_imports::*; + +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Eq, Clone, Copy)] +pub enum Status { + Inactive, + Running, + Ended, +} + + +CARGO.TOML: +[package] +name = "lottery-esdt" +version = "0.0.0" +authors = [ "Dorin Iancu ",] +edition = "2021" +publish = false + +[lib] +path = "src/lottery.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: multisig + +DESCRIPTION: +# Multisig Smart Contract (MSC) + +## Abstract +Cryptocurrencies can be one of the safest ways to store and manage wealth and value. By safeguarding a short list of words, the so-called seed or recovery phrase, anyone can protect thousands or millions of dollars in wealth and rest assured that no hacker or government can take it from them. In practice, it’s never so easy. + +One problem is that single-signature addresses rely on protecting a single private key. + +A better solution would use for example 2-of-3 multisig (or any combination of M-of-N for M ≤ N) quorum consisting of three separate private keys, held by three separate people or entities, and requiring any two to sign. This provides both security and redundancy since compromising any one key/person does not break the quorum: if one key is stolen or lost, the other two keyholders can sweep funds to another address (protected by a new quorum) by mutually signing a transaction moving the funds. + +As an example, let us imagine the following scenario. An institution launches a stablecoin. For safety, it is required that 3 out of 5 designated addresses sign any mint or burn transaction. Alice deploys the multisig SC. She adds Bob, Charlie, Dave and Eve as signers to the contract and sets the quorum to a minimum number of signers to 3. A quorum of signatures is also required to add or remove signers after the initial deployment. If for some reason, Eve’s account is compromised, Alice proposes removing Eve’s address from the signers’ board. Charlie and Dave sign, causing Eve’s address to be removed. There are only 4 addresses now registered in the contract. By the same process, signers could add 2 more addresses to their ranks, and increase the required quorum signatures from 3 to 4. + +Thus, essentially the multisig SC (we will refer to it, from now on, as MSC) enables multiple parties to sign or approve an action that takes place - typically a requirement for certain wallets, accounts, and smart contracts to prevent a rogue or hacked individual from performing detrimental actions. + +## Multisig transaction flow +On-chain multisig wallets are made possible by the fact that smart contracts can call other smart contracts. To execute a multisig transaction the flow would be: + +* A proposer or board member proposes an action. +* The proposed action receives an unique id/hash. +* N board members are notified (off-chain) to review the action with the specific id/hash. +* M out of N board members sign and approve the action. +* Any proposer or board member “performs the action”. + +## Design guidelines + +The required guidelines are: +* **No external contracts.** Calling methods of other contracts from within the methods of your own MSC is an amazing feature but should not be required for our simple use case. This also avoids exposing us to bugs. Because any arbitrarily complex function call can be executed, the MSC functions exactly as a standard wallet, but requires multiple signatures. + +* **No libraries.** Extending the last guideline, our contract has no upstream dependencies other than itself. This minimizes the chance of us misunderstanding or misusing some piece of library code. It also forces us to stay simple and eases auditing and eventually formal verification. + +* **Minimal internal state.** Complex applications can be built inside of MultiversX smart contracts. Storing minimal internal state allows our contract’s code to be simpler, and to be written in a more functional style, which is easier to test and reason about. + +* **Uses cold-storage.** The proposer which creates an action or spends from the contract has no special rights or access to the MSC. Authorization is handled by directly signing messages by the board members’ wallets that can be hardware wallets (Trezor; Ledger, etc.) or software wallets. + +* **Complete end-to-end testing.** The contract itself is exhaustively unit tested, audited and formally verified. + +## Roles +* **Deployer** - This is the address that deploys the MSC. By default this address is also the owner of the SC, but the owner can be changed later if required, as this is by default supported by the MultiversX protocol itself. This is the address that initially set up the configuration of the SC: board members, quorum, etc. It is important to mention that at deployment a very important configuration parameter is the option to allow the SC to be upgradeable or not. It is recommended for most use cases the SC to be non-upgradeable. Leaving the SC upgradable will give the owner of the SC the possibility to upgrade the SC and bypass the board, defeating the purpose of a MSC. If keeping the SC upgradeable is desired, a possible approach would be to make the owner another MSC, and both SCs could maintain the same board, so an upgrade action would need the approval of the board. + +* **Owner** - The deployer is initially the owner of the MSC, but if desired can be changed later by the current owner to a different owner. If the SC is upgradeable, the owner can also upgrade the SC. + +* **Board and quorum** - Multiple addresses need to be previously registered in the MSC, forming its board. A board member needs to be specifically registered as a board member, meaning for example that the owner or deployer of the MSC is not automatically a board member as well. Board members can vote on every action that the MSC performs. Signing a proposed action means the board members agree. Customarily, not all board members will need to sign every action; the MSC will configure how many signatures will be necessary for an action to be performed, the quorum. For instance, such a contract could have 5 board members, but a quorum of 3 would be enough to perform any action (M-of-N or in this case 3-of-5). + +* **Proposer** - The proposer is an address whitelisted in the MSC that can propose any action. An action can be any transaction; for example: send 10 eGLD to the treasury, mint more ESDT, etc. All board members are proposers by default but non-board members can be added as well to the list of whitelisted proposers. The proposers can only propose actions that then need to be approved and signed by the board members. The board member that proposes an action doesn’t need to sign it anymore; it is considered signed. + +## Functionality +The MSC should be able to perform most tasks that a regular account is able to perform. It should also be as general as possible. This means that it should operate with a generic concept of “Action”, that the board needs to sign before being performed. Actions can interact with the MSC itself (let's call them **internal actions**) or with external addresses or other SC (**external actions**). + +External actions have one and only one function, which is to send the action as a transaction whose sender is the MSC. Because any arbitrarily complex function call can be executed, the MSC functions exactly as a standard wallet, but requires multiple signatures. + +The types of internal actions should be the following: + +* Add a new member to the board. +* Remove a member from the board. This is only allowed if the new board size remains larger than the number of required signatures (quorum). Otherwise a new member needs to be added first. +* Change the quorum: the required number of signatures. Restriction: 1 <= quorum <= board size. +* Add a proposer. +* Remove a proposer. +* Change multisig contract owner (might be relevant for upgrading the MSC). +* Pay functions - by default we recommend the MSC to not be set up as a payable SC and any deposit or send transaction of eGLD or ESDT towards the MSC will need to call the desired pay function (if a transaction is not a call to these 2 functions then it is rejected immediately and the value is sent back to original sender): Deposit and/or Send. By making the MSC not a payable MSC we reduce the risk of users sending into the MSC funds that then are locked in the MSC or need to be manually send back to the user (in case of a mistake). By making the MSC not a payable MSC it also means that any deposit or send transaction needs to explicitly call the deposit or send function of the MSC. + +Any external and internal action will follow these steps and process: + +* **Propose action:** this will generate an action id. The action id is unique. +* **View action:** the board members need to see the action proposed before they approve it. +* **Sign action:** board members are allowed to sign. We might add an expiration date until board members can sign (until block x…). +* **Un-sign action:** board members are allowed to un-sign, i.e. to remove their signature from an action. Actions with 0 signatures are cleared from storage. This is to allow mistakes to be cleared. +* **Perform action (by id/hash)** - can be activated by proposers or board members. It is successful only if enough signatures are present from the board members. Whoever calls “perform action” needs to provide any eGLD required by the target, as well as to pay for gas. If there is a move balance kind of action, who calls the action pays the gas and the amount to be moved is taken from MSC balance. But the gas is always taken from the balance of the one who creates the "perform action" transaction. + +Also the following view functions will be available: +* **Count pending Actions:** returns the number of existing Actions. +* **List latest N pending Actions:** provides hashes of the latest N pending Actions, most recent being 0 and oldest being N-1. Usually called in tandem with Count. + +## Initializing the MSC + +There are 2 ways to do it: +* Provide all board member addresses and the number of required signatures directly in the constructor. +* Deployer deploys with just herself on the board and required signatures = 1. Then adds all other N-1 signers and sets required signatures to M. This works, but requires many transactions, so the constructor-only approach might be preferred. + +MSC is a deployable SC written in Rust and compiled in WASM. + +## Conclusion + +Multisig accounts are a critical safety feature for all users of the MultiversX ecosystem. Decentralised applications will rely heavily upon multisig security. + + +SRC FOLDER: +FILE_NAME: action.rs +use multiversx_sc::{ + api::ManagedTypeApi, + types::{BigUint, CodeMetadata, ManagedAddress, ManagedBuffer, ManagedVec}, +}; + +use multiversx_sc::derive_imports::*; + +#[type_abi] +#[derive(NestedEncode, NestedDecode, Clone)] +pub struct CallActionData { + pub to: ManagedAddress, + pub egld_amount: BigUint, + pub endpoint_name: ManagedBuffer, + pub arguments: ManagedVec>, +} + +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, Clone)] +pub enum Action { + Nothing, + AddBoardMember(ManagedAddress), + AddProposer(ManagedAddress), + RemoveUser(ManagedAddress), + ChangeQuorum(usize), + SendTransferExecute(CallActionData), + SendAsyncCall(CallActionData), + SCDeployFromSource { + amount: BigUint, + source: ManagedAddress, + code_metadata: CodeMetadata, + arguments: ManagedVec>, + }, + SCUpgradeFromSource { + sc_address: ManagedAddress, + amount: BigUint, + source: ManagedAddress, + code_metadata: CodeMetadata, + arguments: ManagedVec>, + }, +} + +impl Action { + /// Only pending actions are kept in storage, + /// both executed and discarded actions are removed (converted to `Nothing`). + /// So this is equivalent to `action != Action::Nothing`. + pub fn is_pending(&self) -> bool { + !matches!(*self, Action::Nothing) + } +} + +/// Not used internally, just to retrieve results via endpoint. +#[type_abi] +#[derive(TopEncode)] +pub struct ActionFullInfo { + pub action_id: usize, + pub action_data: Action, + pub signers: ManagedVec>, +} + +#[cfg(test)] +mod test { + use multiversx_sc_scenario::api::StaticApi; + + use super::Action; + + #[test] + fn test_is_pending() { + assert!(!Action::::Nothing.is_pending()); + assert!(Action::::ChangeQuorum(5).is_pending()); + } +} + +FILE_NAME: multisig.rs +#![no_std] + +pub mod action; +pub mod multisig_events; +pub mod multisig_perform; +pub mod multisig_propose; +pub mod multisig_proxy; +pub mod multisig_state; +pub mod multisig_view_proxy; +pub mod user_role; + +use action::ActionFullInfo; +use user_role::UserRole; + +use multiversx_sc::imports::*; + +/// Multi-signature smart contract implementation. +/// Acts like a wallet that needs multiple signers for any action performed. +/// See the readme file for more detailed documentation. +#[multiversx_sc::contract] +pub trait Multisig: + multisig_state::MultisigStateModule + + multisig_propose::MultisigProposeModule + + multisig_perform::MultisigPerformModule + + multisig_events::MultisigEventsModule + + multiversx_sc_modules::dns::DnsModule +{ + #[init] + fn init(&self, quorum: usize, board: MultiValueEncoded) { + let board_vec = board.to_vec(); + let new_num_board_members = self.add_multiple_board_members(board_vec); + + let num_proposers = self.num_proposers().get(); + require!( + new_num_board_members + num_proposers > 0, + "board cannot be empty on init, no-one would be able to propose" + ); + + require!( + quorum <= new_num_board_members, + "quorum cannot exceed board size" + ); + self.quorum().set(quorum); + } + + #[upgrade] + fn upgrade(&self, quorum: usize, board: MultiValueEncoded) { + self.init(quorum, board) + } + + /// Allows the contract to receive funds even if it is marked as unpayable in the protocol. + #[payable("*")] + #[endpoint] + fn deposit(&self) {} + + /// Iterates through all actions and retrieves those that are still pending. + /// Serialized full action data: + /// - the action id + /// - the serialized action data + /// - (number of signers followed by) list of signer addresses. + #[label("multisig-external-view")] + #[view(getPendingActionFullInfo)] + fn get_pending_action_full_info(&self) -> MultiValueEncoded> { + let mut result = MultiValueEncoded::new(); + let action_last_index = self.get_action_last_index(); + let action_mapper = self.action_mapper(); + for action_id in 1..=action_last_index { + let action_data = action_mapper.get(action_id); + if action_data.is_pending() { + result.push(ActionFullInfo { + action_id, + action_data, + signers: self.get_action_signers(action_id), + }); + } + } + result + } + + /// Returns `true` (`1`) if the user has signed the action. + /// Does not check whether or not the user is still a board member and the signature valid. + #[view] + fn signed(&self, user: ManagedAddress, action_id: usize) -> bool { + let user_id = self.user_mapper().get_user_id(&user); + if user_id == 0 { + false + } else { + self.action_signer_ids(action_id).contains(&user_id) + } + } + + /// Indicates user rights. + /// `0` = no rights, + /// `1` = can propose, but not sign, + /// `2` = can propose and sign. + #[label("multisig-external-view")] + #[view(userRole)] + fn user_role(&self, user: ManagedAddress) -> UserRole { + let user_id = self.user_mapper().get_user_id(&user); + if user_id == 0 { + UserRole::None + } else { + self.user_id_to_role(user_id).get() + } + } + + /// Lists all users that can sign actions. + #[label("multisig-external-view")] + #[view(getAllBoardMembers)] + fn get_all_board_members(&self) -> MultiValueEncoded { + self.get_all_users_with_role(UserRole::BoardMember) + } + + /// Lists all proposers that are not board members. + #[label("multisig-external-view")] + #[view(getAllProposers)] + fn get_all_proposers(&self) -> MultiValueEncoded { + self.get_all_users_with_role(UserRole::Proposer) + } + + fn get_all_users_with_role(&self, role: UserRole) -> MultiValueEncoded { + let mut result = MultiValueEncoded::new(); + let num_users = self.user_mapper().get_user_count(); + for user_id in 1..=num_users { + if self.user_id_to_role(user_id).get() == role { + if let Some(address) = self.user_mapper().get_user_address(user_id) { + result.push(address); + } + } + } + result + } + + /// Used by board members to sign actions. + #[endpoint] + fn sign(&self, action_id: usize) { + require!( + !self.action_mapper().item_is_empty_unchecked(action_id), + "action does not exist" + ); + + let (caller_id, caller_role) = self.get_caller_id_and_role(); + require!(caller_role.can_sign(), "only board members can sign"); + + if !self.action_signer_ids(action_id).contains(&caller_id) { + self.action_signer_ids(action_id).insert(caller_id); + } + } + + /// Board members can withdraw their signatures if they no longer desire for the action to be executed. + /// Actions that are left with no valid signatures can be then deleted to free up storage. + #[endpoint] + fn unsign(&self, action_id: usize) { + require!( + !self.action_mapper().item_is_empty_unchecked(action_id), + "action does not exist" + ); + + let (caller_id, caller_role) = self.get_caller_id_and_role(); + require!(caller_role.can_sign(), "only board members can un-sign"); + + self.action_signer_ids(action_id).swap_remove(&caller_id); + } + + /// Clears storage pertaining to an action that is no longer supposed to be executed. + /// Any signatures that the action received must first be removed, via `unsign`. + /// Otherwise this endpoint would be prone to abuse. + #[endpoint(discardAction)] + fn discard_action(&self, action_id: usize) { + let (_, caller_role) = self.get_caller_id_and_role(); + require!( + caller_role.can_discard_action(), + "only board members and proposers can discard actions" + ); + require!( + self.get_action_valid_signer_count(action_id) == 0, + "cannot discard action with valid signatures" + ); + + self.clear_action(action_id); + } +} + +FILE_NAME: multisig_events.rs +use crate::{action::ActionFullInfo, user_role::UserRole}; + +use multiversx_sc::imports::*; + +/// Contains all events that can be emitted by the contract. +#[multiversx_sc::module] +pub trait MultisigEventsModule { + #[event("startPerformAction")] + fn start_perform_action_event(&self, data: &ActionFullInfo); + + #[event("performChangeUser")] + fn perform_change_user_event( + &self, + #[indexed] action_id: usize, + #[indexed] changed_user: &ManagedAddress, + #[indexed] old_role: UserRole, + #[indexed] new_role: UserRole, + ); + + #[event("performChangeQuorum")] + fn perform_change_quorum_event( + &self, + #[indexed] action_id: usize, + #[indexed] new_quorum: usize, + ); + + #[event("performAsyncCall")] + fn perform_async_call_event( + &self, + #[indexed] action_id: usize, + #[indexed] to: &ManagedAddress, + #[indexed] egld_value: &BigUint, + #[indexed] gas: u64, + #[indexed] endpoint: &ManagedBuffer, + #[indexed] arguments: &MultiValueManagedVec, + ); + + #[event("performTransferExecute")] + fn perform_transfer_execute_event( + &self, + #[indexed] action_id: usize, + #[indexed] to: &ManagedAddress, + #[indexed] egld_value: &BigUint, + #[indexed] gas: u64, + #[indexed] endpoint: &ManagedBuffer, + #[indexed] arguments: &MultiValueManagedVec, + ); + + #[event("performDeployFromSource")] + fn perform_deploy_from_source_event( + &self, + #[indexed] action_id: usize, + #[indexed] egld_value: &BigUint, + #[indexed] source_address: &ManagedAddress, + #[indexed] code_metadata: CodeMetadata, + #[indexed] gas: u64, + #[indexed] arguments: &MultiValueManagedVec, + ); + + #[event("performUpgradeFromSource")] + fn perform_upgrade_from_source_event( + &self, + #[indexed] action_id: usize, + #[indexed] target_address: &ManagedAddress, + #[indexed] egld_value: &BigUint, + #[indexed] source_address: &ManagedAddress, + #[indexed] code_metadata: CodeMetadata, + #[indexed] gas: u64, + #[indexed] arguments: &MultiValueManagedVec, + ); +} + +FILE_NAME: multisig_perform.rs +use crate::{ + action::{Action, ActionFullInfo}, + user_role::UserRole, +}; + +use multiversx_sc::imports::*; + +/// Gas required to finish transaction after transfer-execute. +const PERFORM_ACTION_FINISH_GAS: u64 = 300_000; + +fn usize_add_isize(value: &mut usize, delta: isize) { + *value = (*value as isize + delta) as usize; +} + +/// Contains all events that can be emitted by the contract. +#[multiversx_sc::module] +pub trait MultisigPerformModule: + crate::multisig_state::MultisigStateModule + crate::multisig_events::MultisigEventsModule +{ + fn gas_for_transfer_exec(&self) -> u64 { + let gas_left = self.blockchain().get_gas_left(); + if gas_left <= PERFORM_ACTION_FINISH_GAS { + sc_panic!("insufficient gas for call"); + } + gas_left - PERFORM_ACTION_FINISH_GAS + } + + /// Can be used to: + /// - create new user (board member / proposer) + /// - remove user (board member / proposer) + /// - reactivate removed user + /// - convert between board member and proposer + /// Will keep the board size and proposer count in sync. + fn change_user_role(&self, action_id: usize, user_address: ManagedAddress, new_role: UserRole) { + let user_id = if new_role == UserRole::None { + // avoid creating a new user just to delete it + let user_id = self.user_mapper().get_user_id(&user_address); + if user_id == 0 { + return; + } + user_id + } else { + self.user_mapper().get_or_create_user(&user_address) + }; + + let user_id_to_role_mapper = self.user_id_to_role(user_id); + let old_role = user_id_to_role_mapper.get(); + user_id_to_role_mapper.set(new_role); + + self.perform_change_user_event(action_id, &user_address, old_role, new_role); + + // update board size + let mut board_members_delta = 0isize; + if old_role == UserRole::BoardMember { + board_members_delta -= 1; + } + if new_role == UserRole::BoardMember { + board_members_delta += 1; + } + if board_members_delta != 0 { + self.num_board_members() + .update(|value| usize_add_isize(value, board_members_delta)); + } + + let mut proposers_delta = 0isize; + if old_role == UserRole::Proposer { + proposers_delta -= 1; + } + if new_role == UserRole::Proposer { + proposers_delta += 1; + } + if proposers_delta != 0 { + self.num_proposers() + .update(|value| usize_add_isize(value, proposers_delta)); + } + } + + /// Returns `true` (`1`) if `getActionValidSignerCount >= getQuorum`. + #[view(quorumReached)] + fn quorum_reached(&self, action_id: usize) -> bool { + let quorum = self.quorum().get(); + let valid_signers_count = self.get_action_valid_signer_count(action_id); + valid_signers_count >= quorum + } + + fn clear_action(&self, action_id: usize) { + self.action_mapper().clear_entry_unchecked(action_id); + self.action_signer_ids(action_id).clear(); + } + + /// Proposers and board members use this to launch signed actions. + #[endpoint(performAction)] + fn perform_action_endpoint(&self, action_id: usize) -> OptionalValue { + let (_, caller_role) = self.get_caller_id_and_role(); + require!( + caller_role.can_perform_action(), + "only board members and proposers can perform actions" + ); + require!( + self.quorum_reached(action_id), + "quorum has not been reached" + ); + + self.perform_action(action_id) + } + + fn perform_action(&self, action_id: usize) -> OptionalValue { + let action = self.action_mapper().get(action_id); + + self.start_perform_action_event(&ActionFullInfo { + action_id, + action_data: action.clone(), + signers: self.get_action_signers(action_id), + }); + + // clean up storage + // happens before actual execution, because the match provides the return on each branch + // syntax aside, the async_call_raw kills contract execution so cleanup cannot happen afterwards + self.clear_action(action_id); + + match action { + Action::Nothing => OptionalValue::None, + Action::AddBoardMember(board_member_address) => { + self.change_user_role(action_id, board_member_address, UserRole::BoardMember); + OptionalValue::None + }, + Action::AddProposer(proposer_address) => { + self.change_user_role(action_id, proposer_address, UserRole::Proposer); + + // validation required for the scenario when a board member becomes a proposer + require!( + self.quorum().get() <= self.num_board_members().get(), + "quorum cannot exceed board size" + ); + OptionalValue::None + }, + Action::RemoveUser(user_address) => { + self.change_user_role(action_id, user_address, UserRole::None); + let num_board_members = self.num_board_members().get(); + let num_proposers = self.num_proposers().get(); + require!( + num_board_members + num_proposers > 0, + "cannot remove all board members and proposers" + ); + require!( + self.quorum().get() <= num_board_members, + "quorum cannot exceed board size" + ); + OptionalValue::None + }, + Action::ChangeQuorum(new_quorum) => { + require!( + new_quorum <= self.num_board_members().get(), + "quorum cannot exceed board size" + ); + self.quorum().set(new_quorum); + self.perform_change_quorum_event(action_id, new_quorum); + OptionalValue::None + }, + Action::SendTransferExecute(call_data) => { + let gas = self.gas_for_transfer_exec(); + self.perform_transfer_execute_event( + action_id, + &call_data.to, + &call_data.egld_amount, + gas, + &call_data.endpoint_name, + call_data.arguments.as_multi(), + ); + self.tx() + .to(call_data.to) + .egld(call_data.egld_amount) + .gas(gas) + .raw_call(call_data.endpoint_name) + .arguments_raw(call_data.arguments.into()) + .transfer_execute(); + OptionalValue::None + }, + Action::SendAsyncCall(call_data) => { + let gas_left = self.blockchain().get_gas_left(); + self.perform_async_call_event( + action_id, + &call_data.to, + &call_data.egld_amount, + gas_left, + &call_data.endpoint_name, + call_data.arguments.as_multi(), + ); + + self.tx() + .to(&call_data.to) + .raw_call(call_data.endpoint_name) + .arguments_raw(call_data.arguments.into()) + .egld(call_data.egld_amount) + .callback(self.callbacks().perform_async_call_callback()) + .async_call_and_exit(); + }, + Action::SCDeployFromSource { + amount, + source, + code_metadata, + arguments, + } => { + let gas_left = self.blockchain().get_gas_left(); + self.perform_deploy_from_source_event( + action_id, + &amount, + &source, + code_metadata, + gas_left, + arguments.as_multi(), + ); + let new_address = self + .tx() + .egld(amount) + .gas(gas_left) + .raw_deploy() + .from_source(source) + .code_metadata(code_metadata) + .arguments_raw(arguments.into()) + .returns(ReturnsNewManagedAddress) + .sync_call(); + OptionalValue::Some(new_address) + }, + Action::SCUpgradeFromSource { + sc_address, + amount, + source, + code_metadata, + arguments, + } => { + let gas_left = self.blockchain().get_gas_left(); + self.perform_upgrade_from_source_event( + action_id, + &sc_address, + &amount, + &source, + code_metadata, + gas_left, + arguments.as_multi(), + ); + self.tx() + .to(sc_address) + .egld(amount) + .gas(gas_left) + .raw_upgrade() + .from_source(source) + .code_metadata(code_metadata) + .arguments_raw(arguments.into()) + .upgrade_async_call_and_exit(); + OptionalValue::None + }, + } + } + + /// Callback only performs logging. + #[callback] + fn perform_async_call_callback( + &self, + #[call_result] call_result: ManagedAsyncCallResult>, + ) { + match call_result { + ManagedAsyncCallResult::Ok(results) => { + self.async_call_success(results); + }, + ManagedAsyncCallResult::Err(err) => { + self.async_call_error(err.err_code, err.err_msg); + }, + } + } + + #[event("asyncCallSuccess")] + fn async_call_success(&self, #[indexed] results: MultiValueEncoded); + + #[event("asyncCallError")] + fn async_call_error(&self, #[indexed] err_code: u32, #[indexed] err_message: ManagedBuffer); +} + +FILE_NAME: multisig_propose.rs +use crate::action::{Action, CallActionData}; + +use multiversx_sc::imports::*; + +/// Contains all events that can be emitted by the contract. +#[multiversx_sc::module] +pub trait MultisigProposeModule: crate::multisig_state::MultisigStateModule { + fn propose_action(&self, action: Action) -> usize { + let (caller_id, caller_role) = self.get_caller_id_and_role(); + require!( + caller_role.can_propose(), + "only board members and proposers can propose" + ); + + let action_id = self.action_mapper().push(&action); + if caller_role.can_sign() { + // also sign + // since the action is newly created, the caller can be the only signer + self.action_signer_ids(action_id).insert(caller_id); + } + + action_id + } + + /// Initiates board member addition process. + /// Can also be used to promote a proposer to board member. + #[endpoint(proposeAddBoardMember)] + fn propose_add_board_member(&self, board_member_address: ManagedAddress) -> usize { + self.propose_action(Action::AddBoardMember(board_member_address)) + } + + /// Initiates proposer addition process.. + /// Can also be used to demote a board member to proposer. + #[endpoint(proposeAddProposer)] + fn propose_add_proposer(&self, proposer_address: ManagedAddress) -> usize { + self.propose_action(Action::AddProposer(proposer_address)) + } + + /// Removes user regardless of whether it is a board member or proposer. + #[endpoint(proposeRemoveUser)] + fn propose_remove_user(&self, user_address: ManagedAddress) -> usize { + self.propose_action(Action::RemoveUser(user_address)) + } + + #[endpoint(proposeChangeQuorum)] + fn propose_change_quorum(&self, new_quorum: usize) -> usize { + self.propose_action(Action::ChangeQuorum(new_quorum)) + } + + fn prepare_call_data( + &self, + to: ManagedAddress, + egld_amount: BigUint, + function_call: FunctionCall, + ) -> CallActionData { + require!( + egld_amount > 0 || !function_call.is_empty(), + "proposed action has no effect" + ); + + CallActionData { + to, + egld_amount, + endpoint_name: function_call.function_name, + arguments: function_call.arg_buffer.into_vec_of_buffers(), + } + } + + /// Propose a transaction in which the contract will perform a transfer-execute call. + /// Can send EGLD without calling anything. + /// Can call smart contract endpoints directly. + /// Doesn't really work with builtin functions. + #[endpoint(proposeTransferExecute)] + fn propose_transfer_execute( + &self, + to: ManagedAddress, + egld_amount: BigUint, + function_call: FunctionCall, + ) -> usize { + let call_data = self.prepare_call_data(to, egld_amount, function_call); + self.propose_action(Action::SendTransferExecute(call_data)) + } + + /// Propose a transaction in which the contract will perform a transfer-execute call. + /// Can call smart contract endpoints directly. + /// Can use ESDTTransfer/ESDTNFTTransfer/MultiESDTTransfer to send tokens, while also optionally calling endpoints. + /// Works well with builtin functions. + /// Cannot simply send EGLD directly without calling anything. + #[endpoint(proposeAsyncCall)] + fn propose_async_call( + &self, + to: ManagedAddress, + egld_amount: BigUint, + function_call: FunctionCall, + ) -> usize { + let call_data = self.prepare_call_data(to, egld_amount, function_call); + self.propose_action(Action::SendAsyncCall(call_data)) + } + + #[endpoint(proposeSCDeployFromSource)] + fn propose_sc_deploy_from_source( + &self, + amount: BigUint, + source: ManagedAddress, + code_metadata: CodeMetadata, + arguments: MultiValueEncoded, + ) -> usize { + self.propose_action(Action::SCDeployFromSource { + amount, + source, + code_metadata, + arguments: arguments.into_vec_of_buffers(), + }) + } + + #[endpoint(proposeSCUpgradeFromSource)] + fn propose_sc_upgrade_from_source( + &self, + sc_address: ManagedAddress, + amount: BigUint, + source: ManagedAddress, + code_metadata: CodeMetadata, + arguments: MultiValueEncoded, + ) -> usize { + self.propose_action(Action::SCUpgradeFromSource { + sc_address, + amount, + source, + code_metadata, + arguments: arguments.into_vec_of_buffers(), + }) + } +} + +FILE_NAME: multisig_proxy.rs +// 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 MultisigProxy; + +impl TxProxyTrait for MultisigProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = MultisigProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + MultisigProxyMethods { wrapped_tx: tx } + } +} + +pub struct MultisigProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl MultisigProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + pub fn init< + Arg0: ProxyArg, + Arg1: ProxyArg>>, + >( + self, + quorum: Arg0, + board: Arg1, + ) -> TxTypedDeploy { + self.wrapped_tx + .payment(NotPayable) + .raw_deploy() + .argument(&quorum) + .argument(&board) + .original_result() + } +} + +#[rustfmt::skip] +impl MultisigProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn upgrade< + Arg0: ProxyArg, + Arg1: ProxyArg>>, + >( + self, + quorum: Arg0, + board: Arg1, + ) -> TxTypedUpgrade { + self.wrapped_tx + .payment(NotPayable) + .raw_upgrade() + .argument(&quorum) + .argument(&board) + .original_result() + } +} + +#[rustfmt::skip] +impl MultisigProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + /// Allows the contract to receive funds even if it is marked as unpayable in the protocol. + pub fn deposit( + self, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("deposit") + .original_result() + } + + /// Returns `true` (`1`) if the user has signed the action. + /// Does not check whether or not the user is still a board member and the signature valid. + pub fn signed< + Arg0: ProxyArg>, + Arg1: ProxyArg, + >( + self, + user: Arg0, + action_id: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("signed") + .argument(&user) + .argument(&action_id) + .original_result() + } + + /// Used by board members to sign actions. + pub fn sign< + Arg0: ProxyArg, + >( + self, + action_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("sign") + .argument(&action_id) + .original_result() + } + + /// Board members can withdraw their signatures if they no longer desire for the action to be executed. + /// Actions that are left with no valid signatures can be then deleted to free up storage. + pub fn unsign< + Arg0: ProxyArg, + >( + self, + action_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("unsign") + .argument(&action_id) + .original_result() + } + + /// Clears storage pertaining to an action that is no longer supposed to be executed. + /// Any signatures that the action received must first be removed, via `unsign`. + /// Otherwise this endpoint would be prone to abuse. + pub fn discard_action< + Arg0: ProxyArg, + >( + self, + action_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("discardAction") + .argument(&action_id) + .original_result() + } + + /// Minimum number of signatures needed to perform any action. + pub fn quorum( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getQuorum") + .original_result() + } + + /// Denormalized board member count. + /// It is kept in sync with the user list by the contract. + pub fn num_board_members( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getNumBoardMembers") + .original_result() + } + + /// Denormalized proposer count. + /// It is kept in sync with the user list by the contract. + pub fn num_proposers( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getNumProposers") + .original_result() + } + + /// The index of the last proposed action. + /// 0 means that no action was ever proposed yet. + pub fn get_action_last_index( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getActionLastIndex") + .original_result() + } + + /// Initiates board member addition process. + /// Can also be used to promote a proposer to board member. + pub fn propose_add_board_member< + Arg0: ProxyArg>, + >( + self, + board_member_address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("proposeAddBoardMember") + .argument(&board_member_address) + .original_result() + } + + /// Initiates proposer addition process.. + /// Can also be used to demote a board member to proposer. + pub fn propose_add_proposer< + Arg0: ProxyArg>, + >( + self, + proposer_address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("proposeAddProposer") + .argument(&proposer_address) + .original_result() + } + + /// Removes user regardless of whether it is a board member or proposer. + pub fn propose_remove_user< + Arg0: ProxyArg>, + >( + self, + user_address: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("proposeRemoveUser") + .argument(&user_address) + .original_result() + } + + pub fn propose_change_quorum< + Arg0: ProxyArg, + >( + self, + new_quorum: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("proposeChangeQuorum") + .argument(&new_quorum) + .original_result() + } + + /// Propose a transaction in which the contract will perform a transfer-execute call. + /// Can send EGLD without calling anything. + /// Can call smart contract endpoints directly. + /// Doesn't really work with builtin functions. + pub fn propose_transfer_execute< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + Arg2: ProxyArg>, + >( + self, + to: Arg0, + egld_amount: Arg1, + function_call: Arg2, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("proposeTransferExecute") + .argument(&to) + .argument(&egld_amount) + .argument(&function_call) + .original_result() + } + + /// Propose a transaction in which the contract will perform a transfer-execute call. + /// Can call smart contract endpoints directly. + /// Can use ESDTTransfer/ESDTNFTTransfer/MultiESDTTransfer to send tokens, while also optionally calling endpoints. + /// Works well with builtin functions. + /// Cannot simply send EGLD directly without calling anything. + pub fn propose_async_call< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + Arg2: ProxyArg>, + >( + self, + to: Arg0, + egld_amount: Arg1, + function_call: Arg2, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("proposeAsyncCall") + .argument(&to) + .argument(&egld_amount) + .argument(&function_call) + .original_result() + } + + pub fn propose_sc_deploy_from_source< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + Arg2: ProxyArg, + Arg3: ProxyArg>>, + >( + self, + amount: Arg0, + source: Arg1, + code_metadata: Arg2, + arguments: Arg3, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("proposeSCDeployFromSource") + .argument(&amount) + .argument(&source) + .argument(&code_metadata) + .argument(&arguments) + .original_result() + } + + pub fn propose_sc_upgrade_from_source< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + Arg2: ProxyArg>, + Arg3: ProxyArg, + Arg4: ProxyArg>>, + >( + self, + sc_address: Arg0, + amount: Arg1, + source: Arg2, + code_metadata: Arg3, + arguments: Arg4, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("proposeSCUpgradeFromSource") + .argument(&sc_address) + .argument(&amount) + .argument(&source) + .argument(&code_metadata) + .argument(&arguments) + .original_result() + } + + /// Returns `true` (`1`) if `getActionValidSignerCount >= getQuorum`. + pub fn quorum_reached< + Arg0: ProxyArg, + >( + self, + action_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("quorumReached") + .argument(&action_id) + .original_result() + } + + /// Proposers and board members use this to launch signed actions. + pub fn perform_action_endpoint< + Arg0: ProxyArg, + >( + self, + action_id: Arg0, + ) -> TxTypedCall>> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("performAction") + .argument(&action_id) + .original_result() + } + + pub fn dns_register< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + >( + self, + dns_address: Arg0, + name: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("dnsRegister") + .argument(&dns_address) + .argument(&name) + .original_result() + } +} + +#[type_abi] +#[derive(TopEncode)] +pub struct ActionFullInfo +where + Api: ManagedTypeApi, +{ + pub action_id: usize, + pub action_data: Action, + pub signers: ManagedVec>, +} + +#[rustfmt::skip] +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, Clone)] +pub enum Action +where + Api: ManagedTypeApi, +{ + Nothing, + AddBoardMember(ManagedAddress), + AddProposer(ManagedAddress), + RemoveUser(ManagedAddress), + ChangeQuorum(usize), + SendTransferExecute(CallActionData), + SendAsyncCall(CallActionData), + SCDeployFromSource { + amount: BigUint, + source: ManagedAddress, + code_metadata: CodeMetadata, + arguments: ManagedVec>, + }, + SCUpgradeFromSource { + sc_address: ManagedAddress, + amount: BigUint, + source: ManagedAddress, + code_metadata: CodeMetadata, + arguments: ManagedVec>, + }, +} + +#[type_abi] +#[derive(NestedEncode, NestedDecode, Clone)] +pub struct CallActionData +where + Api: ManagedTypeApi, +{ + pub to: ManagedAddress, + pub egld_amount: BigUint, + pub endpoint_name: ManagedBuffer, + pub arguments: ManagedVec>, +} + +#[type_abi] +#[derive(TopEncode, TopDecode, Clone, Copy, PartialEq, Eq, Debug)] +pub enum UserRole { + None, + Proposer, + BoardMember, +} + +FILE_NAME: multisig_state.rs +use crate::{action::Action, user_role::UserRole}; + +use multiversx_sc::imports::*; + +/// Contains all events that can be emitted by the contract. +#[multiversx_sc::module] +pub trait MultisigStateModule { + /// Minimum number of signatures needed to perform any action. + #[view(getQuorum)] + #[storage_mapper("quorum")] + fn quorum(&self) -> SingleValueMapper; + + #[storage_mapper("user")] + fn user_mapper(&self) -> UserMapper; + + #[storage_mapper("user_role")] + fn user_id_to_role(&self, user_id: usize) -> SingleValueMapper; + + fn get_caller_id_and_role(&self) -> (usize, UserRole) { + let caller_address = self.blockchain().get_caller(); + let caller_id = self.user_mapper().get_user_id(&caller_address); + let caller_role = self.user_id_to_role(caller_id).get(); + (caller_id, caller_role) + } + + /// Denormalized board member count. + /// It is kept in sync with the user list by the contract. + #[view(getNumBoardMembers)] + #[storage_mapper("num_board_members")] + fn num_board_members(&self) -> SingleValueMapper; + + /// Denormalized proposer count. + /// It is kept in sync with the user list by the contract. + #[view(getNumProposers)] + #[storage_mapper("num_proposers")] + fn num_proposers(&self) -> SingleValueMapper; + + fn add_multiple_board_members(&self, new_board_members: ManagedVec) -> usize { + let mut duplicates = false; + self.user_mapper().get_or_create_users( + new_board_members.into_iter(), + |user_id, new_user| { + if !new_user { + duplicates = true; + } + self.user_id_to_role(user_id).set(UserRole::BoardMember); + }, + ); + require!(!duplicates, "duplicate board member"); + + let num_board_members_mapper = self.num_board_members(); + let new_num_board_members = num_board_members_mapper.get() + new_board_members.len(); + num_board_members_mapper.set(new_num_board_members); + + new_num_board_members + } + + #[storage_mapper("action_data")] + fn action_mapper(&self) -> VecMapper>; + + /// The index of the last proposed action. + /// 0 means that no action was ever proposed yet. + #[view(getActionLastIndex)] + fn get_action_last_index(&self) -> usize { + self.action_mapper().len() + } + + /// Serialized action data of an action with index. + #[label("multisig-external-view")] + #[view(getActionData)] + fn get_action_data(&self, action_id: usize) -> Action { + self.action_mapper().get(action_id) + } + + #[storage_mapper("action_signer_ids")] + fn action_signer_ids(&self, action_id: usize) -> UnorderedSetMapper; + + /// Gets addresses of all users who signed an action. + /// Does not check if those users are still board members or not, + /// so the result may contain invalid signers. + #[label("multisig-external-view")] + #[view(getActionSigners)] + fn get_action_signers(&self, action_id: usize) -> ManagedVec { + let signer_ids = self.action_signer_ids(action_id); + let mut signers = ManagedVec::new(); + for signer_id in signer_ids.iter() { + signers.push(self.user_mapper().get_user_address_unchecked(signer_id)); + } + signers + } + + /// Gets addresses of all users who signed an action and are still board members. + /// All these signatures are currently valid. + #[label("multisig-external-view")] + #[view(getActionSignerCount)] + fn get_action_signer_count(&self, action_id: usize) -> usize { + self.action_signer_ids(action_id).len() + } + + /// It is possible for board members to lose their role. + /// They are not automatically removed from all actions when doing so, + /// therefore the contract needs to re-check every time when actions are performed. + /// This function is used to validate the signers before performing an action. + /// It also makes it easy to check before performing an action. + #[label("multisig-external-view")] + #[view(getActionValidSignerCount)] + fn get_action_valid_signer_count(&self, action_id: usize) -> usize { + let signer_ids = self.action_signer_ids(action_id); + signer_ids + .iter() + .filter(|signer_id| { + let signer_role = self.user_id_to_role(*signer_id).get(); + signer_role.can_sign() + }) + .count() + } +} + +FILE_NAME: multisig_view_proxy.rs +// 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 MultisigProxy; + +impl TxProxyTrait for MultisigProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = MultisigProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + MultisigProxyMethods { wrapped_tx: tx } + } +} + +pub struct MultisigProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl MultisigProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + /// Iterates through all actions and retrieves those that are still pending. + /// Serialized full action data: + /// - the action id + /// - the serialized action data + /// - (number of signers followed by) list of signer addresses. + pub fn get_pending_action_full_info( + self, + ) -> TxTypedCall>> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getPendingActionFullInfo") + .original_result() + } + + /// Indicates user rights. + /// `0` = no rights, + /// `1` = can propose, but not sign, + /// `2` = can propose and sign. + pub fn user_role< + Arg0: ProxyArg>, + >( + self, + user: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("userRole") + .argument(&user) + .original_result() + } + + /// Lists all users that can sign actions. + pub fn get_all_board_members( + self, + ) -> TxTypedCall>> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getAllBoardMembers") + .original_result() + } + + /// Lists all proposers that are not board members. + pub fn get_all_proposers( + self, + ) -> TxTypedCall>> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getAllProposers") + .original_result() + } + + /// Serialized action data of an action with index. + pub fn get_action_data< + Arg0: ProxyArg, + >( + self, + action_id: Arg0, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getActionData") + .argument(&action_id) + .original_result() + } + + /// Gets addresses of all users who signed an action. + /// Does not check if those users are still board members or not, + /// so the result may contain invalid signers. + pub fn get_action_signers< + Arg0: ProxyArg, + >( + self, + action_id: Arg0, + ) -> TxTypedCall>> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getActionSigners") + .argument(&action_id) + .original_result() + } + + /// Gets addresses of all users who signed an action and are still board members. + /// All these signatures are currently valid. + pub fn get_action_signer_count< + Arg0: ProxyArg, + >( + self, + action_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getActionSignerCount") + .argument(&action_id) + .original_result() + } + + /// It is possible for board members to lose their role. + /// They are not automatically removed from all actions when doing so, + /// therefore the contract needs to re-check every time when actions are performed. + /// This function is used to validate the signers before performing an action. + /// It also makes it easy to check before performing an action. + pub fn get_action_valid_signer_count< + Arg0: ProxyArg, + >( + self, + action_id: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getActionValidSignerCount") + .argument(&action_id) + .original_result() + } +} + +#[type_abi] +#[derive(TopEncode)] +pub struct ActionFullInfo +where + Api: ManagedTypeApi, +{ + pub action_id: usize, + pub action_data: Action, + pub signers: ManagedVec>, +} + +#[rustfmt::skip] +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, Clone)] +pub enum Action +where + Api: ManagedTypeApi, +{ + Nothing, + AddBoardMember(ManagedAddress), + AddProposer(ManagedAddress), + RemoveUser(ManagedAddress), + ChangeQuorum(usize), + SendTransferExecute(CallActionData), + SendAsyncCall(CallActionData), + SCDeployFromSource { + amount: BigUint, + source: ManagedAddress, + code_metadata: CodeMetadata, + arguments: ManagedVec>, + }, + SCUpgradeFromSource { + sc_address: ManagedAddress, + amount: BigUint, + source: ManagedAddress, + code_metadata: CodeMetadata, + arguments: ManagedVec>, + }, +} + +#[type_abi] +#[derive(NestedEncode, NestedDecode, Clone)] +pub struct CallActionData +where + Api: ManagedTypeApi, +{ + pub to: ManagedAddress, + pub egld_amount: BigUint, + pub endpoint_name: ManagedBuffer, + pub arguments: ManagedVec>, +} + +#[type_abi] +#[derive(TopEncode, TopDecode, Clone, Copy, PartialEq, Eq, Debug)] +pub enum UserRole { + None, + Proposer, + BoardMember, +} + +FILE_NAME: user_role.rs +use multiversx_sc::derive_imports::*; + +#[type_abi] +#[derive(TopEncode, TopDecode, Clone, Copy, PartialEq, Eq, Debug)] +pub enum UserRole { + None, + Proposer, + BoardMember, +} + +impl UserRole { + pub fn can_propose(&self) -> bool { + matches!(*self, UserRole::BoardMember | UserRole::Proposer) + } + + pub fn can_perform_action(&self) -> bool { + self.can_propose() + } + + pub fn can_discard_action(&self) -> bool { + self.can_propose() + } + + pub fn can_sign(&self) -> bool { + matches!(*self, UserRole::BoardMember) + } +} + + +CARGO.TOML: +[package] +name = "multisig" +version = "1.0.0" +authors = ["Andrei Marinica "] +edition = "2021" +publish = false + +[lib] +path = "src/multisig.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dependencies.multiversx-sc-modules] +version = "0.54.6" +path = "../../../contracts/modules" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + +[dev-dependencies.adder] +path = "../adder" + +[dev-dependencies.factorial] +path = "../factorial" + +[dev-dependencies.multiversx-wegld-swap-sc] +version = "0.54.6" +path = "../../core/wegld-swap" + +[dev-dependencies] +num-bigint = "0.4" +num-traits = "0.2" +hex = "0.4" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: nft-minter + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: lib.rs +#![no_std] + +use multiversx_sc::{derive_imports::*, imports::*}; + +pub mod nft_marketplace_proxy; +mod nft_module; + +#[type_abi] +#[derive(TopEncode, TopDecode)] +pub struct ExampleAttributes { + pub creation_timestamp: u64, +} + +#[multiversx_sc::contract] +pub trait NftMinter: nft_module::NftModule { + #[init] + fn init(&self) {} + + #[allow_multiple_var_args] + #[allow(clippy::too_many_arguments)] + #[allow(clippy::redundant_closure)] + #[only_owner] + #[endpoint(createNft)] + fn create_nft( + &self, + name: ManagedBuffer, + royalties: BigUint, + uri: ManagedBuffer, + selling_price: BigUint, + opt_token_used_as_payment: OptionalValue, + opt_token_used_as_payment_nonce: OptionalValue, + ) { + let token_used_as_payment = match opt_token_used_as_payment { + OptionalValue::Some(token) => EgldOrEsdtTokenIdentifier::esdt(token), + OptionalValue::None => EgldOrEsdtTokenIdentifier::egld(), + }; + require!( + token_used_as_payment.is_valid(), + "Invalid token_used_as_payment arg, not a valid token ID" + ); + + let token_used_as_payment_nonce = if token_used_as_payment.is_egld() { + 0 + } else { + match opt_token_used_as_payment_nonce { + OptionalValue::Some(nonce) => nonce, + OptionalValue::None => 0, + } + }; + + let attributes = ExampleAttributes { + creation_timestamp: self.blockchain().get_block_timestamp(), + }; + self.create_nft_with_attributes( + name, + royalties, + attributes, + uri, + selling_price, + token_used_as_payment, + token_used_as_payment_nonce, + ); + } + + // The marketplace SC will send the funds directly to the initial caller, i.e. the owner + // The caller has to know which tokens they have to claim, + // by giving the correct token ID and token nonce + #[only_owner] + #[endpoint(claimRoyaltiesFromMarketplace)] + fn claim_royalties_from_marketplace( + &self, + marketplace_address: ManagedAddress, + token_id: TokenIdentifier, + token_nonce: u64, + ) { + let caller = self.blockchain().get_caller(); + self.tx() + .to(&marketplace_address) + .typed(nft_marketplace_proxy::NftMarketplaceProxy) + .claim_tokens(token_id, token_nonce, caller) + .async_call_and_exit(); + } +} + +FILE_NAME: nft_marketplace_proxy.rs +use multiversx_sc::proxy_imports::*; + +pub struct NftMarketplaceProxy; + +impl TxProxyTrait for NftMarketplaceProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = NftMarketplaceProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + NftMarketplaceProxyMethods { wrapped_tx: tx } + } +} + +pub struct NftMarketplaceProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl NftMarketplaceProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn claim_tokens< + Arg0: ProxyArg>, + Arg1: ProxyArg, + Arg2: ProxyArg>, + >( + self, + token_id: Arg0, + token_nonce: Arg1, + claim_destination: Arg2, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("claimTokens") + .argument(&token_id) + .argument(&token_nonce) + .argument(&claim_destination) + .original_result() + } +} + +FILE_NAME: nft_module.rs +use multiversx_sc::{derive_imports::*, imports::*}; + +const NFT_AMOUNT: u32 = 1; +const ROYALTIES_MAX: u32 = 10_000; + +#[type_abi] +#[derive(TopEncode, TopDecode)] +pub struct PriceTag { + pub token: EgldOrEsdtTokenIdentifier, + pub nonce: u64, + pub amount: BigUint, +} + +#[multiversx_sc::module] +pub trait NftModule { + // endpoints - owner-only + + #[only_owner] + #[payable("EGLD")] + #[endpoint(issueToken)] + fn issue_token(&self, token_name: ManagedBuffer, token_ticker: ManagedBuffer) { + require!(self.nft_token_id().is_empty(), "Token already issued"); + + let payment_amount = self.call_value().egld_value(); + self.send() + .esdt_system_sc_proxy() + .issue_non_fungible( + payment_amount.clone_value(), + &token_name, + &token_ticker, + NonFungibleTokenProperties { + can_freeze: true, + can_wipe: true, + can_pause: true, + can_transfer_create_role: true, + can_change_owner: false, + can_upgrade: false, + can_add_special_roles: true, + }, + ) + .with_callback(self.callbacks().issue_callback()) + .async_call_and_exit() + } + + #[only_owner] + #[endpoint(setLocalRoles)] + fn set_local_roles(&self) { + self.require_token_issued(); + + self.send() + .esdt_system_sc_proxy() + .set_special_roles( + self.blockchain().get_sc_address(), + self.nft_token_id().get(), + [EsdtLocalRole::NftCreate][..].iter().cloned(), + ) + .async_call_and_exit() + } + + // endpoints + + #[payable("*")] + #[endpoint(buyNft)] + fn buy_nft(&self, nft_nonce: u64) { + let payment = self.call_value().egld_or_single_esdt(); + + self.require_token_issued(); + require!( + !self.price_tag(nft_nonce).is_empty(), + "Invalid nonce or NFT was already sold" + ); + + let price_tag = self.price_tag(nft_nonce).get(); + require!( + payment.token_identifier == price_tag.token, + "Invalid token used as payment" + ); + require!( + payment.token_nonce == price_tag.nonce, + "Invalid nonce for payment token" + ); + require!( + payment.amount == price_tag.amount, + "Invalid amount as payment" + ); + + self.price_tag(nft_nonce).clear(); + + let nft_token_id = self.nft_token_id().get(); + + self.tx() + .to(ToCaller) + .single_esdt(&nft_token_id, nft_nonce, &BigUint::from(NFT_AMOUNT)) + .transfer(); + + let owner = self.blockchain().get_owner_address(); + self.tx().to(owner).payment(payment).transfer(); + } + + // views + + #[allow(clippy::type_complexity)] + #[view(getNftPrice)] + fn get_nft_price( + &self, + nft_nonce: u64, + ) -> OptionalValue> { + if self.price_tag(nft_nonce).is_empty() { + // NFT was already sold + OptionalValue::None + } else { + let price_tag = self.price_tag(nft_nonce).get(); + + OptionalValue::Some((price_tag.token, price_tag.nonce, price_tag.amount).into()) + } + } + + // callbacks + + #[callback] + fn issue_callback( + &self, + #[call_result] result: ManagedAsyncCallResult, + ) { + match result { + ManagedAsyncCallResult::Ok(token_id) => { + self.nft_token_id().set(token_id.unwrap_esdt()); + }, + ManagedAsyncCallResult::Err(_) => { + let returned = self.call_value().egld_or_single_esdt(); + if returned.token_identifier.is_egld() && returned.amount > 0 { + self.tx().to(ToCaller).egld(returned.amount).transfer(); + } + }, + } + } + + // private + + #[allow(clippy::too_many_arguments)] + fn create_nft_with_attributes( + &self, + name: ManagedBuffer, + royalties: BigUint, + attributes: T, + uri: ManagedBuffer, + selling_price: BigUint, + token_used_as_payment: EgldOrEsdtTokenIdentifier, + token_used_as_payment_nonce: u64, + ) -> u64 { + self.require_token_issued(); + require!(royalties <= ROYALTIES_MAX, "Royalties cannot exceed 100%"); + + let nft_token_id = self.nft_token_id().get(); + + let mut serialized_attributes = ManagedBuffer::new(); + if let core::result::Result::Err(err) = attributes.top_encode(&mut serialized_attributes) { + sc_panic!("Attributes encode error: {}", err.message_bytes()); + } + + let attributes_sha256 = self.crypto().sha256(&serialized_attributes); + let attributes_hash = attributes_sha256.as_managed_buffer(); + let uris = ManagedVec::from_single_item(uri); + let nft_nonce = self.send().esdt_nft_create( + &nft_token_id, + &BigUint::from(NFT_AMOUNT), + &name, + &royalties, + attributes_hash, + &attributes, + &uris, + ); + + self.price_tag(nft_nonce).set(&PriceTag { + token: token_used_as_payment, + nonce: token_used_as_payment_nonce, + amount: selling_price, + }); + + nft_nonce + } + + fn require_token_issued(&self) { + require!(!self.nft_token_id().is_empty(), "Token not issued"); + } + + // storage + + #[storage_mapper("nftTokenId")] + fn nft_token_id(&self) -> SingleValueMapper; + + #[storage_mapper("priceTag")] + fn price_tag(&self, nft_nonce: u64) -> SingleValueMapper>; +} + + +CARGO.TOML: +[package] +name = "nft-minter" +version = "0.0.0" +authors = ["Dorin Iancu "] +edition = "2021" +publish = false + +[lib] +path = "src/lib.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: nft-storage-prepay + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: nft_storage_prepay.rs +#![no_std] + +use multiversx_sc::imports::*; + +#[multiversx_sc::contract] +pub trait NftStoragePrepay { + #[init] + fn init(&self, cost_per_byte: BigUint) { + self.cost_per_byte().set(&cost_per_byte); + } + + // endpoints - owner-only + + #[only_owner] + #[endpoint(setCostPerByte)] + fn set_cost_per_byte(&self, cost_per_byte: BigUint) { + self.cost_per_byte().set(&cost_per_byte); + } + + #[only_owner] + #[endpoint(reserveFunds)] + fn reserve_funds(&self, address: ManagedAddress, file_size: BigUint) { + let storage_cost = self.get_cost_for_size(file_size); + let mut user_deposit = self.deposit(&address).get(); + require!( + user_deposit >= storage_cost, + "User does not have enough deposit" + ); + + user_deposit -= &storage_cost; + self.deposit(&address).set(&user_deposit); + self.total_reserved() + .update(|reserved| *reserved += storage_cost); + } + + #[only_owner] + #[endpoint] + fn claim(&self) { + let total_reserved = self.total_reserved().get(); + require!(total_reserved > 0u32, "Nothing to claim"); + + self.total_reserved().clear(); + + let owner = self.blockchain().get_caller(); + self.tx().to(&owner).egld(&total_reserved).transfer(); + } + + // endpoints + + #[payable("EGLD")] + #[endpoint(depositPaymentForStorage)] + fn deposit_payment_for_storage(&self) { + let payment = self.call_value().egld_value(); + let caller = self.blockchain().get_caller(); + self.deposit(&caller) + .update(|deposit| *deposit += &*payment); + } + + /// defaults to max amount + #[endpoint(withdraw)] + fn withdraw(&self, opt_amount: OptionalValue) { + let caller = self.blockchain().get_caller(); + let mut user_deposit = self.deposit(&caller).get(); + let amount = match opt_amount { + OptionalValue::Some(amt) => amt, + OptionalValue::None => user_deposit.clone(), + }; + + require!(user_deposit >= amount, "Can't withdraw more than deposit"); + + user_deposit -= &amount; + self.deposit(&caller).set(&user_deposit); + + self.tx().to(&caller).egld(&amount).transfer(); + } + + // views + + #[view(getCostForSize)] + fn get_cost_for_size(&self, file_size: BigUint) -> BigUint { + let cost_per_byte = self.cost_per_byte().get(); + + file_size * cost_per_byte + } + + #[view(getDepositAmount)] + fn get_deposit_amount(&self) -> BigUint { + let caller = self.blockchain().get_caller(); + + self.deposit(&caller).get() + } + + // storage + + #[view(getCostPerByte)] + #[storage_mapper("costPerByte")] + fn cost_per_byte(&self) -> SingleValueMapper; + + #[storage_mapper("deposit")] + fn deposit(&self, address: &ManagedAddress) -> SingleValueMapper; + + #[storage_mapper("totalReserved")] + fn total_reserved(&self) -> SingleValueMapper; +} + + +CARGO.TOML: +[package] +name = "nft-storage-prepay" +version = "0.0.0" +authors = [ "Dorin Iancu ",] +edition = "2021" +publish = false + +[lib] +path = "src/nft_storage_prepay.rs" + + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: nft-subscription + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: lib.rs +#![no_std] + +use multiversx_sc::imports::*; + +use multiversx_sc_modules::{default_issue_callbacks, subscription}; + +#[multiversx_sc::contract] +pub trait NftSubscription: + default_issue_callbacks::DefaultIssueCallbacksModule + subscription::SubscriptionModule +{ + #[init] + fn init(&self) {} + + #[endpoint] + fn issue(&self) { + self.token_id().issue_and_set_all_roles( + EsdtTokenType::NonFungible, + self.call_value().egld_value().clone_value(), + ManagedBuffer::from(b"Subscription"), + ManagedBuffer::from(b"SUB"), + 0, + None, + ) + } + + #[endpoint] + fn mint(&self) { + let nonce = self.create_subscription_nft( + self.token_id().get_token_id_ref(), + &BigUint::from(1u8), + &ManagedBuffer::new(), + &BigUint::from(0u8), + &ManagedBuffer::new(), + 0, + ManagedBuffer::from(b"common"), + &ManagedVec::new(), + ); + + self.tx() + .to(ToCaller) + .single_esdt( + self.token_id().get_token_id_ref(), + nonce, + &BigUint::from(1u8), + ) + .transfer(); + } + + #[payable("*")] + #[endpoint] + fn update_attributes(&self, attributes: ManagedBuffer) { + let (id, nonce, _) = self.call_value().single_esdt().into_tuple(); + self.update_subscription_attributes::(&id, nonce, attributes); + self.tx() + .to(ToCaller) + .single_esdt(&id, nonce, &BigUint::from(1u8)) + .transfer(); + } + + #[payable("*")] + #[endpoint] + fn renew(&self, duration: u64) { + let (id, nonce, _) = self.call_value().single_esdt().into_tuple(); + self.renew_subscription::(&id, nonce, duration); + self.tx() + .to(ToCaller) + .single_esdt(&id, nonce, &BigUint::from(1u8)) + .transfer(); + } + + #[payable("*")] + #[endpoint] + fn cancel(&self) { + let (id, nonce, _) = self.call_value().single_esdt().into_tuple(); + self.cancel_subscription::(&id, nonce); + + self.tx() + .to(ToCaller) + .single_esdt(&id, nonce, &BigUint::from(1u8)) + .transfer(); + } + + #[storage_mapper("tokenId")] + fn token_id(&self) -> NonFungibleTokenMapper; +} + + +CARGO.TOML: +[package] +name = "nft-subscription" +version = "0.0.0" +authors = ["Thouny "] +edition = "2021" +publish = false + +[lib] +path = "src/lib.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dependencies.multiversx-sc-modules] +version = "0.54.6" +path = "../../../contracts/modules" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: order-book + +DESCRIPTION: +None + +SRC FOLDER: + +INTERACTOR FOLDER: None +//////////////////////// +NAME: ping-pong-egld + +DESCRIPTION: +# PingPong + +`PingPong` is a simple Smart Contract. + + +SRC FOLDER: +FILE_NAME: ping_pong.rs +#![no_std] + +use multiversx_sc::imports::*; + +pub mod proxy_ping_pong_egld; +mod types; + +use types::{ContractState, UserStatus}; + +/// Derived empirically. +const PONG_ALL_LOW_GAS_LIMIT: u64 = 3_000_000; + +/// A contract that allows anyone to send a fixed sum, locks it for a while and then allows users to take it back. +/// Sending funds to the contract is called "ping". +/// Taking the same funds back is called "pong". +/// +/// Restrictions: +/// - `ping` can be called only after the contract is activated. By default the contract is activated on deploy. +/// - Users can only `ping` once, ever. +/// - Only the set amount can be `ping`-ed, no more, no less. +/// - The contract can optionally have a maximum cap. No more users can `ping` after the cap has been reached. +/// - The `ping` endpoint optionally accepts +/// - `pong` can only be called after the contract expired (a certain duration has passed since activation). +/// - `pongAll` can be used to send to all users to `ping`-ed. If it runs low on gas, it will interrupt itself. +/// It can be continued anytime. +#[multiversx_sc::contract] +pub trait PingPong { + /// 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. + #[allow_multiple_var_args] + #[init] + fn init( + &self, + ping_amount: &BigUint, + duration_in_seconds: u64, + opt_activation_timestamp: Option, + max_funds: OptionalValue, + ) { + self.ping_amount().set(ping_amount); + let activation_timestamp = + opt_activation_timestamp.unwrap_or_else(|| self.blockchain().get_block_timestamp()); + let deadline = activation_timestamp + duration_in_seconds; + self.deadline().set(deadline); + self.activation_timestamp().set(activation_timestamp); + 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")] + #[endpoint] + fn ping(&self, _data: IgnoreValue) { + let payment = self.call_value().egld_value(); + + require!( + *payment == self.ping_amount().get(), + "the payment must match the fixed sum" + ); + + let block_timestamp = self.blockchain().get_block_timestamp(); + require!( + self.activation_timestamp().get() <= block_timestamp, + "smart contract not active yet" + ); + + require!( + block_timestamp < self.deadline().get(), + "deadline has passed" + ); + + if let Some(max_funds) = self.max_funds().get() { + require!( + &self + .blockchain() + .get_sc_balance(&EgldOrEsdtTokenIdentifier::egld(), 0) + + &*payment + <= max_funds, + "smart contract full" + ); + } + + let caller = self.blockchain().get_caller(); + let user_id = self.user_mapper().get_or_create_user(&caller); + let user_status = self.user_status(user_id).get(); + 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> { + let user_status = self.user_status(user_id).get(); + match user_status { + UserStatus::New => Result::Err("can't pong, never pinged"), + UserStatus::Registered => { + self.user_status(user_id).set(UserStatus::Withdrawn); + if let Some(user_address) = self.user_mapper().get_user_address(user_id) { + 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"), + } + } + + /// User can take back funds from the contract. + /// Can only be called after expiration. + #[endpoint] + fn pong(&self) { + require!( + self.blockchain().get_block_timestamp() >= self.deadline().get(), + "can't withdraw before deadline" + ); + + let caller = self.blockchain().get_caller(); + let user_id = self.user_mapper().get_user_id(&caller); + let pong_result = self.pong_by_user_id(user_id); + if let Result::Err(message) = pong_result { + sc_panic!(message); + } + } + + /// 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. + #[endpoint(pongAll)] + fn pong_all(&self) -> OperationCompletionStatus { + let now = self.blockchain().get_block_timestamp(); + require!( + 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); + 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); + break; + } + + pong_all_last_user += 1; + + // 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, + /// in the order they have `ping`-ed + #[view(getUserAddresses)] + fn get_user_addresses(&self) -> MultiValueEncoded { + 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)] + #[storage_mapper("pingAmount")] + fn ping_amount(&self) -> SingleValueMapper; + + #[view(getDeadline)] + #[storage_mapper("deadline")] + fn deadline(&self) -> SingleValueMapper; + + /// Block timestamp of the block where the contract got activated. + /// If not specified in the constructor it is the the deploy block timestamp. + #[view(getActivationTimestamp)] + #[storage_mapper("activationTimestamp")] + fn activation_timestamp(&self) -> SingleValueMapper; + + /// Optional funding cap. + #[view(getMaxFunds)] + #[storage_mapper("maxFunds")] + fn max_funds(&self) -> SingleValueMapper>; + + #[storage_mapper("user")] + fn user_mapper(&self) -> UserMapper; + + /// State of user funds. + /// 0 - user unknown, never `ping`-ed + /// 1 - `ping`-ed + /// 2 - `pong`-ed + #[view(getUserStatus)] + #[storage_mapper("userStatus")] + 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. + #[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, + ); +} + +FILE_NAME: proxy_ping_pong_egld.rs +// 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, +} + +FILE_NAME: types.rs +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, +} + + +CARGO.TOML: +[package] +name = "ping-pong-egld" +version = "0.0.2" +authors = [ "Bruda Claudiu-Marcel ",] +edition = "2021" +publish = false + +[lib] +path = "src/ping_pong.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: proxy-pause + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: pause_sc_proxy.rs +use multiversx_sc::proxy_imports::*; + +pub struct PausableProxy; + +impl TxProxyTrait for PausableProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = PausableProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + PausableProxyMethods { wrapped_tx: tx } + } +} + +pub struct PausableProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl PausableProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn pause( + self, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("pause") + .original_result() + } + + pub fn unpause( + self, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("unpause") + .original_result() + } +} + +FILE_NAME: proxy_pause.rs +#![no_std] + +use multiversx_sc::imports::*; +pub mod pause_sc_proxy; + +#[multiversx_sc::contract] +pub trait PauseProxy { + #[init] + fn init(&self) { + self.owners().insert(self.blockchain().get_caller()); + } + + #[endpoint(addContracts)] + fn add_contracts(&self, contracts: MultiValueEncoded) { + self.require_owner(); + self.contracts().extend(contracts); + } + + #[endpoint(removeContracts)] + fn remove_contracts(&self, contracts: MultiValueEncoded) { + self.require_owner(); + self.contracts().remove_all(contracts); + } + + #[endpoint(addOwners)] + fn add_owners(&self, owners: MultiValueEncoded) { + self.require_owner(); + self.owners().extend(owners); + } + + #[endpoint(removeOwners)] + fn remove_owners(&self, owners: MultiValueEncoded) { + self.require_owner(); + self.owners().remove_all(owners); + } + + fn for_each_contract(&self, f: F) + where + F: Fn(pause_sc_proxy::PausableProxyMethods, (), &ManagedAddress, ()>), + { + for contract_address in self.contracts().iter() { + f(self + .tx() + .to(&contract_address) + .typed(pause_sc_proxy::PausableProxy)); + } + } + + #[endpoint] + fn pause(&self) { + self.require_owner(); + self.for_each_contract(|contract| contract.pause().sync_call()); + } + + #[endpoint] + fn unpause(&self) { + self.require_owner(); + self.for_each_contract(|contract| contract.unpause().sync_call()); + } + + fn require_owner(&self) { + require!( + self.owners().contains(&self.blockchain().get_caller()), + "caller is not an owner" + ); + } + + #[view] + #[storage_mapper("owners")] + fn owners(&self) -> SetMapper; + + #[view] + #[storage_mapper("contracts")] + fn contracts(&self) -> SetMapper; +} + + +CARGO.TOML: +[package] +name = "proxy-pause" +version = "0.0.0" +authors = [ "you",] +edition = "2021" +publish = false + +[lib] +path = "src/proxy_pause.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + +[dev-dependencies.check-pause] +path = "../check-pause" + + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: rewards-distribution + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: rewards_distribution.rs +#![no_std] + +use multiversx_sc::{derive_imports::*, imports::*}; +use multiversx_sc_modules::ongoing_operation::{ + CONTINUE_OP, DEFAULT_MIN_GAS_TO_SAVE_PROGRESS, STOP_OP, +}; + +pub mod rewards_distribution_proxy; +pub mod seed_nft_minter_proxy; +type Epoch = u64; + +pub const EPOCHS_IN_WEEK: Epoch = 7; +pub const MAX_PERCENTAGE: u64 = 100_000; // 100% +pub const DIVISION_SAFETY_CONSTANT: u64 = 1_000_000_000_000; + +#[type_abi] +#[derive(ManagedVecItem, NestedEncode, NestedDecode)] +pub struct Bracket { + pub index_percent: u64, + pub bracket_reward_percent: u64, +} + +#[type_abi] +#[derive(ManagedVecItem, NestedEncode, NestedDecode)] +pub struct ComputedBracket { + pub end_index: u64, + pub nft_reward_percent: BigUint, +} + +#[derive(NestedEncode, NestedDecode)] +pub struct RaffleProgress { + pub raffle_id: u64, + pub ticket_position: u64, + pub ticket_count: u64, + pub computed_brackets: ManagedVec>, +} + +#[multiversx_sc::contract] +pub trait RewardsDistribution: + multiversx_sc_modules::ongoing_operation::OngoingOperationModule +{ + #[init] + fn init(&self, seed_nft_minter_address: ManagedAddress, brackets: ManagedVec) { + self.seed_nft_minter_address().set(&seed_nft_minter_address); + + let nft_token_id = self + .tx() + .to(&seed_nft_minter_address) + .typed(seed_nft_minter_proxy::SeedNftMinterProxy) + .nft_token_id() + .returns(ReturnsResult) + .sync_call(); + + self.nft_token_id().set(nft_token_id); + + self.validate_brackets(&brackets); + self.brackets().set(brackets); + } + + #[payable("*")] + #[endpoint(depositRoyalties)] + fn deposit_royalties(&self) { + let payment = self.call_value().egld_or_single_esdt(); + let raffle_id = self.raffle_id().get(); + self.royalties(raffle_id, &payment.token_identifier, payment.token_nonce) + .update(|total| *total += payment.amount); + } + + #[endpoint(raffle)] + fn raffle(&self) -> OperationCompletionStatus { + let mut raffle = self + .raffle_progress() + .get() + .unwrap_or_else(|| self.new_raffle()); + let mut rng = RandomnessSource::default(); + + let mut bracket = raffle.computed_brackets.get(0); + + let run_result = self.run_while_it_has_gas(DEFAULT_MIN_GAS_TO_SAVE_PROGRESS, || { + let ticket = self.shuffle_and_pick_single_ticket( + &mut rng, + raffle.ticket_position, + raffle.ticket_count, + ); + self.try_advance_bracket( + &mut bracket, + &mut raffle.computed_brackets, + raffle.ticket_position, + ); + + self.nft_reward_percent(raffle.raffle_id, ticket) + .update(|nft_reward_percent| *nft_reward_percent += &bracket.nft_reward_percent); + + if raffle.ticket_position == raffle.ticket_count { + return STOP_OP; + } + + raffle.ticket_position += 1; + + CONTINUE_OP + }); + + let raffle_progress = match run_result { + OperationCompletionStatus::InterruptedBeforeOutOfGas => Some(raffle), + OperationCompletionStatus::Completed => { + self.completed_raffle_id_count().set(raffle.raffle_id + 1); + None + }, + }; + + self.raffle_progress().set(raffle_progress); + + run_result + } + + fn validate_brackets(&self, brackets: &ManagedVec) { + let index_total: u64 = brackets.iter().map(|bracket| bracket.index_percent).sum(); + require!( + index_total == MAX_PERCENTAGE, + "Index percent total must be 100%" + ); + + let reward_total: u64 = brackets + .iter() + .map(|bracket| bracket.bracket_reward_percent) + .sum(); + require!( + reward_total == MAX_PERCENTAGE, + "Reward percent total must be 100%" + ); + } + + fn try_advance_bracket( + &self, + bracket: &mut ComputedBracket, + computed_brackets: &mut ManagedVec>, + ticket: u64, + ) { + while ticket > bracket.end_index { + computed_brackets.remove(0); + *bracket = computed_brackets.get(0); + } + } + + /// Fisher-Yates algorithm, + /// each position i is swapped with a random one in range [i, n] + /// + /// After shuffling, the ticket at the current position is taken and returned + fn shuffle_and_pick_single_ticket( + &self, + rng: &mut RandomnessSource, + current_ticket_position: u64, + ticket_count: u64, + ) -> u64 { + let rand_pos = rng.next_u64_in_range(current_ticket_position, ticket_count + 1); + + let current_ticket_id = self.take_ticket(current_ticket_position); + if rand_pos == current_ticket_position { + current_ticket_id + } else { + self.replace_ticket(rand_pos, current_ticket_id) + } + } + + fn take_ticket(&self, position: u64) -> u64 { + let id = self.tickets(position).take(); + ticket_from_storage(position, id) + } + + fn replace_ticket(&self, position: u64, new_ticket_id: u64) -> u64 { + let id_to_save = ticket_to_storage(position, new_ticket_id); + let loaded_id = self.tickets(position).replace(id_to_save); + ticket_from_storage(position, loaded_id) + } + + fn new_raffle(&self) -> RaffleProgress { + self.require_new_raffle_period(); + + let raffle_id = self.raffle_id().update(|raffle_id| { + let last_id = *raffle_id; + *raffle_id += 1; + last_id + }); + + let seed_nft_minter_address = self.seed_nft_minter_address().get(); + + let ticket_count = self + .tx() + .to(&seed_nft_minter_address) + .typed(seed_nft_minter_proxy::SeedNftMinterProxy) + .nft_count() + .returns(ReturnsResult) + .sync_call(); + + let brackets = self.brackets().get(); + + let computed_brackets = self.compute_brackets(brackets, ticket_count); + + let ticket_position = 1; + + RaffleProgress { + raffle_id, + ticket_position, + ticket_count, + computed_brackets, + } + } + + fn compute_brackets( + &self, + brackets: ManagedVec, + ticket_count: u64, + ) -> ManagedVec> { + require!(ticket_count > 0, "No tickets"); + + let mut computed_brackets = ManagedVec::new(); + let mut index_cutoff_percent = 0; + + let mut start_index = 0; + for bracket in &brackets { + index_cutoff_percent += bracket.index_percent; + let end_index = ticket_count * index_cutoff_percent / MAX_PERCENTAGE; + let count = end_index - start_index; + start_index = end_index; + require!(count > 0, "Invalid bracket"); + let nft_reward_percent = + BigUint::from(bracket.bracket_reward_percent) * DIVISION_SAFETY_CONSTANT / count; + + computed_brackets.push(ComputedBracket { + end_index, + nft_reward_percent, + }); + } + + computed_brackets + } + + fn require_new_raffle_period(&self) { + let current_epoch = self.blockchain().get_block_epoch(); + let last_raffle_epoch = self.last_raffle_epoch().replace(current_epoch); + if last_raffle_epoch == 0 { + return; + } + require!( + last_raffle_epoch + EPOCHS_IN_WEEK <= current_epoch, + "Last raffle was less than one week ago" + ); + } + + #[payable("*")] + #[endpoint(claimRewards)] + fn claim_rewards( + &self, + raffle_id_start: u64, + raffle_id_end: u64, + reward_tokens: MultiValueEncoded>, + ) { + let nfts = self.call_value().all_esdt_transfers(); + self.validate_nft_payments(&nfts); + self.validate_raffle_id_range(raffle_id_start, raffle_id_end); + + let caller = self.blockchain().get_caller(); + let mut rewards = ManagedVec::new(); + let mut total_egld_reward = BigUint::zero(); + + for reward_token_pair in reward_tokens.into_iter() { + let (reward_token_id, reward_token_nonce) = reward_token_pair.into_tuple(); + let (egld_reward, reward_payment_opt) = self.claim_reward_token( + raffle_id_start, + raffle_id_end, + &reward_token_id, + reward_token_nonce, + &nfts, + ); + + total_egld_reward += egld_reward; + if let Some(reward_payment) = reward_payment_opt { + rewards.push(reward_payment); + } + } + + self.tx().to(&caller).egld(total_egld_reward).transfer(); + self.tx().to(&caller).payment(rewards).transfer(); + self.tx().to(&caller).payment(nfts).transfer(); + } + + fn claim_reward_token( + &self, + raffle_id_start: u64, + raffle_id_end: u64, + reward_token_id: &EgldOrEsdtTokenIdentifier, + reward_token_nonce: u64, + nfts: &ManagedVec, + ) -> (BigUint, Option) { + let mut total = BigUint::zero(); + + for raffle_id in raffle_id_start..=raffle_id_end { + for nft in nfts { + let claim_result = + self.try_claim(raffle_id, reward_token_id, reward_token_nonce, &nft); + if claim_result.is_err() { + continue; + } + + total += self.compute_claimable_amount( + raffle_id, + reward_token_id, + reward_token_nonce, + nft.token_nonce, + ); + } + } + + if total == 0 || reward_token_id.is_egld() { + return (total, None); + } + let reward_payment = EsdtTokenPayment::new( + reward_token_id.clone().unwrap_esdt(), + reward_token_nonce, + total, + ); + (BigUint::zero(), Some(reward_payment)) + } + + fn try_claim( + &self, + raffle_id: u64, + reward_token_id: &EgldOrEsdtTokenIdentifier, + reward_token_nonce: u64, + nft: &EsdtTokenPayment, + ) -> Result<(), ()> { + let was_claimed_mapper = self.was_claimed( + raffle_id, + reward_token_id, + reward_token_nonce, + nft.token_nonce, + ); + let available = !was_claimed_mapper.get(); + if available { + was_claimed_mapper.set(true); + Result::Ok(()) + } else { + Result::Err(()) + } + } + + #[view(computeClaimableAmount)] + fn compute_claimable_amount( + &self, + raffle_id: u64, + reward_token_id: &EgldOrEsdtTokenIdentifier, + reward_token_nonce: u64, + nft_nonce: u64, + ) -> BigUint { + let nft_reward_percent = self.nft_reward_percent(raffle_id, nft_nonce).get(); + let royalties = self + .royalties(raffle_id, reward_token_id, reward_token_nonce) + .get(); + royalties * nft_reward_percent / MAX_PERCENTAGE / DIVISION_SAFETY_CONSTANT + } + + fn validate_nft_payments(&self, nfts: &ManagedVec) { + let nft_token_id = self.nft_token_id().get(); + require!(!nfts.is_empty(), "Missing payment"); + for nft in nfts { + require!(nft.token_identifier == nft_token_id, "Invalid payment"); + } + } + + fn validate_raffle_id_range(&self, start: u64, end: u64) { + require!(start <= end, "Invalid range"); + + let completed_raffle_id_count = self.completed_raffle_id_count().get(); + require!(end < completed_raffle_id_count, "Invalid raffle id end"); + } + + #[view(getRaffleId)] + #[storage_mapper("raffleId")] + fn raffle_id(&self) -> SingleValueMapper; + + #[view(getCompletedRaffleIdCount)] + #[storage_mapper("completedRaffleIdCount")] + fn completed_raffle_id_count(&self) -> SingleValueMapper; + + #[view(getRoyalties)] + #[storage_mapper("royalties")] + fn royalties( + &self, + raffle_id: u64, + reward_token_id: &EgldOrEsdtTokenIdentifier, + reward_token_nonce: u64, + ) -> SingleValueMapper; + + #[view(getNftRewardPercent)] + #[storage_mapper("nftRewardPercent")] + fn nft_reward_percent(&self, raffle_id: u64, nft_nonce: u64) -> SingleValueMapper; + + #[view(getWasClaimed)] + #[storage_mapper("wasClaimed")] + fn was_claimed( + &self, + raffle_id: u64, + reward_token_id: &EgldOrEsdtTokenIdentifier, + reward_token_nonce: u64, + nft_nonce: u64, + ) -> SingleValueMapper; + + #[view(getSeedNftMinterAddress)] + #[storage_mapper("seedNftMinterAddress")] + fn seed_nft_minter_address(&self) -> SingleValueMapper; + + #[view(getBrackets)] + #[storage_mapper("brackets")] + fn brackets(&self) -> SingleValueMapper>; + + #[view(getLastRaffleEpoch)] + #[storage_mapper("lastRaffleEpoch")] + fn last_raffle_epoch(&self) -> SingleValueMapper; + + #[view(getNftTokenId)] + #[storage_mapper("nftTokenIdentifier")] + fn nft_token_id(&self) -> SingleValueMapper; + + #[storage_mapper("tickets")] + fn tickets(&self, position: u64) -> SingleValueMapper; + + #[storage_mapper("currentTicketId")] + fn current_ticket_id(&self) -> SingleValueMapper; + + #[storage_mapper("raffleProgress")] + fn raffle_progress(&self) -> SingleValueMapper>>; +} + +fn ticket_to_storage(position: u64, ticket_id: u64) -> u64 { + if position == ticket_id { + 0 + } else { + ticket_id + } +} + +fn ticket_from_storage(position: u64, ticket_id: u64) -> u64 { + if ticket_id == 0 { + position + } else { + ticket_id + } +} + +FILE_NAME: rewards_distribution_proxy.rs +// 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 RewardsDistributionProxy; + +impl TxProxyTrait for RewardsDistributionProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = RewardsDistributionProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + RewardsDistributionProxyMethods { wrapped_tx: tx } + } +} + +pub struct RewardsDistributionProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl RewardsDistributionProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + pub fn init< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + >( + self, + seed_nft_minter_address: Arg0, + brackets: Arg1, + ) -> TxTypedDeploy { + self.wrapped_tx + .payment(NotPayable) + .raw_deploy() + .argument(&seed_nft_minter_address) + .argument(&brackets) + .original_result() + } +} + +#[rustfmt::skip] +impl RewardsDistributionProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn deposit_royalties( + self, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("depositRoyalties") + .original_result() + } + + pub fn raffle( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("raffle") + .original_result() + } + + pub fn claim_rewards< + Arg0: ProxyArg, + Arg1: ProxyArg, + Arg2: ProxyArg, u64>>>, + >( + self, + raffle_id_start: Arg0, + raffle_id_end: Arg1, + reward_tokens: Arg2, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("claimRewards") + .argument(&raffle_id_start) + .argument(&raffle_id_end) + .argument(&reward_tokens) + .original_result() + } + + pub fn compute_claimable_amount< + Arg0: ProxyArg, + Arg1: ProxyArg>, + Arg2: ProxyArg, + Arg3: ProxyArg, + >( + self, + raffle_id: Arg0, + reward_token_id: Arg1, + reward_token_nonce: Arg2, + nft_nonce: Arg3, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("computeClaimableAmount") + .argument(&raffle_id) + .argument(&reward_token_id) + .argument(&reward_token_nonce) + .argument(&nft_nonce) + .original_result() + } + + pub fn raffle_id( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getRaffleId") + .original_result() + } + + pub fn completed_raffle_id_count( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getCompletedRaffleIdCount") + .original_result() + } + + pub fn royalties< + Arg0: ProxyArg, + Arg1: ProxyArg>, + Arg2: ProxyArg, + >( + self, + raffle_id: Arg0, + reward_token_id: Arg1, + reward_token_nonce: Arg2, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getRoyalties") + .argument(&raffle_id) + .argument(&reward_token_id) + .argument(&reward_token_nonce) + .original_result() + } + + pub fn nft_reward_percent< + Arg0: ProxyArg, + Arg1: ProxyArg, + >( + self, + raffle_id: Arg0, + nft_nonce: Arg1, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getNftRewardPercent") + .argument(&raffle_id) + .argument(&nft_nonce) + .original_result() + } + + pub fn was_claimed< + Arg0: ProxyArg, + Arg1: ProxyArg>, + Arg2: ProxyArg, + Arg3: ProxyArg, + >( + self, + raffle_id: Arg0, + reward_token_id: Arg1, + reward_token_nonce: Arg2, + nft_nonce: Arg3, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getWasClaimed") + .argument(&raffle_id) + .argument(&reward_token_id) + .argument(&reward_token_nonce) + .argument(&nft_nonce) + .original_result() + } + + pub fn seed_nft_minter_address( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getSeedNftMinterAddress") + .original_result() + } + + pub fn brackets( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getBrackets") + .original_result() + } + + pub fn last_raffle_epoch( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getLastRaffleEpoch") + .original_result() + } + + pub fn nft_token_id( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getNftTokenId") + .original_result() + } +} + +#[type_abi] +#[derive(ManagedVecItem, NestedEncode, NestedDecode)] +pub struct Bracket { + pub index_percent: u64, + pub bracket_reward_percent: u64, +} + +FILE_NAME: seed_nft_minter_proxy.rs +// 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 SeedNftMinterProxy; + +impl TxProxyTrait for SeedNftMinterProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = SeedNftMinterProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + SeedNftMinterProxyMethods { wrapped_tx: tx } + } +} + +pub struct SeedNftMinterProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl SeedNftMinterProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + pub fn init< + Arg0: ProxyArg>>, + Arg1: ProxyArg>>, + >( + self, + marketplaces: Arg0, + distribution: Arg1, + ) -> TxTypedDeploy { + self.wrapped_tx + .payment(NotPayable) + .raw_deploy() + .argument(&marketplaces) + .argument(&distribution) + .original_result() + } +} + +#[rustfmt::skip] +impl SeedNftMinterProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn create_nft< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + Arg2: ProxyArg>, + Arg3: ProxyArg>, + Arg4: ProxyArg>>, + Arg5: ProxyArg>, + >( + self, + name: Arg0, + royalties: Arg1, + uri: Arg2, + selling_price: Arg3, + opt_token_used_as_payment: Arg4, + opt_token_used_as_payment_nonce: Arg5, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("createNft") + .argument(&name) + .argument(&royalties) + .argument(&uri) + .argument(&selling_price) + .argument(&opt_token_used_as_payment) + .argument(&opt_token_used_as_payment_nonce) + .original_result() + } + + pub fn claim_and_distribute< + Arg0: ProxyArg>, + Arg1: ProxyArg, + >( + self, + token_id: Arg0, + token_nonce: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("claimAndDistribute") + .argument(&token_id) + .argument(&token_nonce) + .original_result() + } + + pub fn marketplaces( + self, + ) -> TxTypedCall>> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getMarketplaces") + .original_result() + } + + pub fn nft_count( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getNftCount") + .original_result() + } + + pub fn distribution_rules( + self, + ) -> TxTypedCall>> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getDistributionRules") + .original_result() + } + + pub fn issue_token< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + >( + self, + token_display_name: Arg0, + token_ticker: Arg1, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("issueToken") + .argument(&token_display_name) + .argument(&token_ticker) + .original_result() + } + + pub fn buy_nft< + Arg0: ProxyArg, + >( + self, + nft_nonce: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("buyNft") + .argument(&nft_nonce) + .original_result() + } + + pub fn get_nft_price< + Arg0: ProxyArg, + >( + self, + nft_nonce: Arg0, + ) -> TxTypedCall, u64, BigUint>>> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getNftPrice") + .argument(&nft_nonce) + .original_result() + } + + pub fn nft_token_id( + self, + ) -> TxTypedCall> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("getNftTokenId") + .original_result() + } +} + +#[type_abi] +#[derive(ManagedVecItem, NestedEncode, NestedDecode)] +pub struct Distribution +where + Api: ManagedTypeApi, +{ + pub address: ManagedAddress, + pub percentage: u64, + pub endpoint: ManagedBuffer, + pub gas_limit: u64, +} + + +CARGO.TOML: +[package] +name = "rewards-distribution" +version = "0.0.0" +authors = ["Claudiu-Marcel Bruda "] +edition = "2021" +publish = false + +[lib] +path = "src/rewards_distribution.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dependencies.multiversx-sc-modules] +version = "0.54.6" +path = "../../../contracts/modules" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: seed-nft-minter + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: distribution_module.rs +use multiversx_sc::{derive_imports::*, imports::*}; + +pub const MAX_DISTRIBUTION_PERCENTAGE: u64 = 100_000; // 100% + +#[type_abi] +#[derive(ManagedVecItem, NestedEncode, NestedDecode)] +pub struct Distribution { + pub address: ManagedAddress, + pub percentage: u64, + pub endpoint: ManagedBuffer, + pub gas_limit: u64, +} + +#[multiversx_sc::module] +pub trait DistributionModule { + fn init_distribution(&self, distribution: ManagedVec>) { + self.validate_distribution(&distribution); + self.distribution_rules().set(distribution); + } + + fn distribute_funds( + &self, + token_id: &EgldOrEsdtTokenIdentifier, + token_nonce: u64, + total_amount: BigUint, + ) { + if total_amount == 0 { + return; + } + for distribution in self.distribution_rules().get().iter() { + let payment_amount = + &total_amount * distribution.percentage / MAX_DISTRIBUTION_PERCENTAGE; + if payment_amount == 0 { + continue; + } + self.tx() + .to(&distribution.address) + .raw_call(distribution.endpoint) + .egld_or_single_esdt(token_id, token_nonce, &payment_amount) + .gas(distribution.gas_limit) + .transfer_execute(); + } + } + + fn validate_distribution(&self, distribution: &ManagedVec>) { + let index_total: u64 = distribution + .iter() + .map(|distribution| distribution.percentage) + .sum(); + require!( + index_total == MAX_DISTRIBUTION_PERCENTAGE, + "Distribution percent total must be 100%" + ); + } + + #[view(getDistributionRules)] + #[storage_mapper("distributionRules")] + fn distribution_rules(&self) -> SingleValueMapper>>; +} + +FILE_NAME: nft_marketplace_proxy.rs +#![allow(clippy::all)] + +use multiversx_sc::proxy_imports::*; + +pub struct NftMarketplaceProxy; + +impl TxProxyTrait for NftMarketplaceProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = NftMarketplaceProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + NftMarketplaceProxyMethods { wrapped_tx: tx } + } +} + +pub struct NftMarketplaceProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl NftMarketplaceProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + pub fn init( + self, + ) -> TxTypedDeploy { + self.wrapped_tx + .raw_deploy() + .original_result() + } +} + +#[rustfmt::skip] +impl NftMarketplaceProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn claim_tokens< + Arg0: ProxyArg>, + Arg1: ProxyArg>, + Arg2: ProxyArg, + >( + self, + claim_destination: Arg0, + token_id: Arg1, + token_nonce: Arg2, + ) -> TxTypedCall, ManagedVec>>> { + self.wrapped_tx + .raw_call("claimTokens") + .argument(&claim_destination) + .argument(&token_id) + .argument(&token_nonce) + .original_result() + } +} + +FILE_NAME: nft_module.rs +use crate::distribution_module; + +use multiversx_sc::{derive_imports::*, imports::*}; + +use multiversx_sc_modules::default_issue_callbacks; + +const NFT_AMOUNT: u32 = 1; +const ROYALTIES_MAX: u32 = 10_000; // 100% + +#[type_abi] +#[derive(TopEncode, TopDecode)] +pub struct PriceTag { + pub token: EgldOrEsdtTokenIdentifier, + pub nonce: u64, + pub amount: BigUint, +} + +#[multiversx_sc::module] +pub trait NftModule: + distribution_module::DistributionModule + default_issue_callbacks::DefaultIssueCallbacksModule +{ + // endpoints - owner-only + + #[only_owner] + #[payable("EGLD")] + #[endpoint(issueToken)] + fn issue_token(&self, token_display_name: ManagedBuffer, token_ticker: ManagedBuffer) { + let issue_cost = self.call_value().egld_value(); + self.nft_token_id().issue_and_set_all_roles( + EsdtTokenType::NonFungible, + issue_cost.clone_value(), + token_display_name, + token_ticker, + 0, + None, + ); + } + + // endpoints + + #[payable("*")] + #[endpoint(buyNft)] + fn buy_nft(&self, nft_nonce: u64) { + let payment = self.call_value().egld_or_single_esdt(); + + self.require_token_issued(); + require!( + !self.price_tag(nft_nonce).is_empty(), + "Invalid nonce or NFT was already sold" + ); + + let price_tag = self.price_tag(nft_nonce).get(); + require!( + payment.token_identifier == price_tag.token, + "Invalid token used as payment" + ); + require!( + payment.token_nonce == price_tag.nonce, + "Invalid nonce for payment token" + ); + require!( + payment.amount == price_tag.amount, + "Invalid amount as payment" + ); + + self.price_tag(nft_nonce).clear(); + + let nft_token_id = self.nft_token_id().get_token_id(); + + self.tx() + .to(ToCaller) + .single_esdt(&nft_token_id, nft_nonce, &BigUint::from(NFT_AMOUNT)) + .transfer(); + + self.distribute_funds( + &payment.token_identifier, + payment.token_nonce, + payment.amount, + ); + } + + // views + + #[allow(clippy::type_complexity)] + #[view(getNftPrice)] + fn get_nft_price( + &self, + nft_nonce: u64, + ) -> OptionalValue> { + if self.price_tag(nft_nonce).is_empty() { + // NFT was already sold + OptionalValue::None + } else { + let price_tag = self.price_tag(nft_nonce).get(); + + OptionalValue::Some((price_tag.token, price_tag.nonce, price_tag.amount).into()) + } + } + + // private + + #[allow(clippy::too_many_arguments)] + fn create_nft_with_attributes( + &self, + name: ManagedBuffer, + royalties: BigUint, + attributes: T, + uri: ManagedBuffer, + selling_price: BigUint, + token_used_as_payment: EgldOrEsdtTokenIdentifier, + token_used_as_payment_nonce: u64, + ) -> u64 { + self.require_token_issued(); + require!(royalties <= ROYALTIES_MAX, "Royalties cannot exceed 100%"); + + let nft_token_id = self.nft_token_id().get_token_id(); + + let mut serialized_attributes = ManagedBuffer::new(); + if let core::result::Result::Err(err) = attributes.top_encode(&mut serialized_attributes) { + sc_panic!("Attributes encode error: {}", err.message_bytes()); + } + + let attributes_sha256 = self.crypto().sha256(&serialized_attributes); + let attributes_hash = attributes_sha256.as_managed_buffer(); + let uris = ManagedVec::from_single_item(uri); + let nft_nonce = self.send().esdt_nft_create( + &nft_token_id, + &BigUint::from(NFT_AMOUNT), + &name, + &royalties, + attributes_hash, + &attributes, + &uris, + ); + + self.price_tag(nft_nonce).set(&PriceTag { + token: token_used_as_payment, + nonce: token_used_as_payment_nonce, + amount: selling_price, + }); + + nft_nonce + } + + fn require_token_issued(&self) { + require!(!self.nft_token_id().is_empty(), "Token not issued"); + } + + // storage + + #[view(getNftTokenId)] + #[storage_mapper("nftTokenId")] + fn nft_token_id(&self) -> NonFungibleTokenMapper; + + #[storage_mapper("priceTag")] + fn price_tag(&self, nft_nonce: u64) -> SingleValueMapper>; +} + +FILE_NAME: seed_nft_minter.rs +#![no_std] + +use multiversx_sc::{derive_imports::*, imports::*}; + +mod distribution_module; +pub mod nft_marketplace_proxy; +mod nft_module; + +use distribution_module::Distribution; +use multiversx_sc_modules::default_issue_callbacks; + +#[type_abi] +#[derive(TopEncode, TopDecode)] +pub struct ExampleAttributes { + pub creation_timestamp: u64, +} + +#[multiversx_sc::contract] +pub trait SeedNftMinter: + distribution_module::DistributionModule + + nft_module::NftModule + + default_issue_callbacks::DefaultIssueCallbacksModule +{ + #[init] + fn init( + &self, + marketplaces: ManagedVec, + distribution: ManagedVec>, + ) { + self.marketplaces().extend(&marketplaces); + self.init_distribution(distribution); + } + + #[allow_multiple_var_args] + #[only_owner] + #[endpoint(createNft)] + fn create_nft( + &self, + name: ManagedBuffer, + royalties: BigUint, + uri: ManagedBuffer, + selling_price: BigUint, + opt_token_used_as_payment: OptionalValue, + opt_token_used_as_payment_nonce: OptionalValue, + ) { + let token_used_as_payment = match opt_token_used_as_payment { + OptionalValue::Some(token) => EgldOrEsdtTokenIdentifier::esdt(token), + OptionalValue::None => EgldOrEsdtTokenIdentifier::egld(), + }; + require!( + token_used_as_payment.is_valid(), + "Invalid token_used_as_payment arg, not a valid token ID" + ); + + let token_used_as_payment_nonce = if token_used_as_payment.is_egld() { + 0 + } else { + match opt_token_used_as_payment_nonce { + OptionalValue::Some(nonce) => nonce, + OptionalValue::None => 0, + } + }; + + let attributes = ExampleAttributes { + creation_timestamp: self.blockchain().get_block_timestamp(), + }; + let nft_nonce = self.create_nft_with_attributes( + name, + royalties, + attributes, + uri, + selling_price, + token_used_as_payment, + token_used_as_payment_nonce, + ); + + self.nft_count().set(nft_nonce); + } + + #[only_owner] + #[endpoint(claimAndDistribute)] + fn claim_and_distribute(&self, token_id: EgldOrEsdtTokenIdentifier, token_nonce: u64) { + let total_amount = self.claim_royalties(&token_id, token_nonce); + self.distribute_funds(&token_id, token_nonce, total_amount); + } + + fn claim_royalties(&self, token_id: &EgldOrEsdtTokenIdentifier, token_nonce: u64) -> BigUint { + let claim_destination = self.blockchain().get_sc_address(); + let mut total_amount = BigUint::zero(); + for address in self.marketplaces().iter() { + let results = self + .tx() + .to(&address) + .typed(nft_marketplace_proxy::NftMarketplaceProxy) + .claim_tokens(&claim_destination, token_id, token_nonce) + .returns(ReturnsResult) + .sync_call(); + + let (egld_amount, esdt_payments) = results.into_tuple(); + let amount = if token_id.is_egld() { + egld_amount + } else { + esdt_payments + .try_get(0) + .map(|esdt_payment| esdt_payment.amount) + .unwrap_or_default() + }; + total_amount += amount; + } + + total_amount + } + + #[view(getMarketplaces)] + #[storage_mapper("marketplaces")] + fn marketplaces(&self) -> UnorderedSetMapper; + + #[view(getNftCount)] + #[storage_mapper("nftCount")] + fn nft_count(&self) -> SingleValueMapper; +} + + +CARGO.TOML: +[package] +name = "seed-nft-minter" +version = "0.0.0" +authors = ["Claudiu-Marcel Bruda "] +edition = "2021" +publish = false + +[lib] +path = "src/seed_nft_minter.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dependencies.multiversx-sc-modules] +version = "0.54.6" +path = "../../../contracts/modules" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + +INTERACTOR FOLDER: None +//////////////////////// +NAME: token-release + +DESCRIPTION: +None + +SRC FOLDER: +FILE_NAME: contract_data.rs +use multiversx_sc::{api::ManagedTypeApi, types::BigUint}; + +use multiversx_sc::derive_imports::*; + +#[type_abi] +#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, PartialEq, Eq, Clone)] +pub enum UnlockType { + FixedAmount { + period_unlock_amount: BigUint, + release_period: u64, + release_ticks: u64, + }, + Percentage { + period_unlock_percentage: u8, + release_period: u64, + release_ticks: u64, + }, +} + +#[type_abi] +#[derive(TopEncode, TopDecode, NestedEncode, NestedDecode, PartialEq, Eq, Clone)] +pub struct Schedule { + pub group_total_amount: BigUint, + pub unlock_type: UnlockType, +} + +FILE_NAME: token_release.rs +#![no_std] + +use multiversx_sc::imports::*; + +mod contract_data; + +use contract_data::{Schedule, UnlockType}; + +const PERCENTAGE_TOTAL: u64 = 100; + +#[multiversx_sc::contract] +pub trait TokenRelease { + // The SC initializes with the setup period started. After the initial setup, the SC offers a function that ends the setup period. + // There is no function to start the setup period back on, so once the setup period is ended, it cannot be changed. + #[init] + fn init(&self, token_identifier: TokenIdentifier) { + require!( + token_identifier.is_valid_esdt_identifier(), + "Invalid token provided" + ); + self.token_identifier().set(&token_identifier); + self.setup_period_status().set(true); + } + + // endpoints + + // Workflow + // First, all groups are defined. After that, an address can be assigned as many groups as needed + #[only_owner] + #[endpoint(addFixedAmountGroup)] + fn add_fixed_amount_group( + &self, + group_identifier: ManagedBuffer, + group_total_amount: BigUint, + period_unlock_amount: BigUint, + release_period: u64, + release_ticks: u64, + ) { + self.require_setup_period_live(); + require!( + self.group_schedule(&group_identifier).is_empty(), + "The group already exists" + ); + require!( + release_ticks > 0u64, + "The schedule must have at least 1 unlock period" + ); + require!( + group_total_amount > BigUint::zero(), + "The schedule must have a positive number of total tokens released" + ); + require!( + &period_unlock_amount * &BigUint::from(release_ticks) == group_total_amount, + "The total number of tokens is invalid" + ); + + self.token_total_supply() + .update(|total| *total += &group_total_amount); + let unlock_type = UnlockType::FixedAmount { + period_unlock_amount, + release_period, + release_ticks, + }; + let new_schedule = Schedule { + group_total_amount, + unlock_type, + }; + self.group_schedule(&group_identifier).set(&new_schedule); + } + + #[only_owner] + #[endpoint(addPercentageBasedGroup)] + fn add_percentage_based_group( + &self, + group_identifier: ManagedBuffer, + group_total_amount: BigUint, + period_unlock_percentage: u8, + release_period: u64, + release_ticks: u64, + ) { + self.require_setup_period_live(); + require!( + self.group_schedule(&group_identifier).is_empty(), + "The group already exists" + ); + require!( + release_ticks > 0_u64, + "The schedule must have at least 1 unlock period" + ); + require!( + group_total_amount > BigUint::zero(), + "The schedule must have a positive number of total tokens released" + ); + require!( + (period_unlock_percentage as u64) * release_ticks == PERCENTAGE_TOTAL, + "The final percentage is invalid" + ); + + self.token_total_supply() + .update(|total| *total += &group_total_amount); + let unlock_type = UnlockType::Percentage { + period_unlock_percentage, + release_period, + release_ticks, + }; + let new_schedule = Schedule { + group_total_amount, + unlock_type, + }; + self.group_schedule(&group_identifier).set(&new_schedule); + } + + #[only_owner] + #[endpoint(removeGroup)] + fn remove_group(&self, group_identifier: ManagedBuffer) { + self.require_setup_period_live(); + require!( + !self.group_schedule(&group_identifier).is_empty(), + "The group does not exist" + ); + + let schedule = self.group_schedule(&group_identifier).get(); + self.token_total_supply() + .update(|total| *total -= &schedule.group_total_amount); + self.group_schedule(&group_identifier).clear(); + self.users_in_group(&group_identifier).clear(); + } + + #[only_owner] + #[endpoint(addUserGroup)] + fn add_user_group(&self, address: ManagedAddress, group_identifier: ManagedBuffer) { + self.require_setup_period_live(); + require!( + !self.group_schedule(&group_identifier).is_empty(), + "The group does not exist" + ); + + self.user_groups(&address).update(|groups| { + let mut group_exists = false; + for group in groups.iter() { + if group == group_identifier.as_ref() { + group_exists = true; + break; + } + } + if !group_exists { + self.users_in_group(&group_identifier) + .update(|users_in_group_no| *users_in_group_no += 1); + groups.push(group_identifier); + } + }); + } + + #[only_owner] + #[endpoint(removeUser)] + fn remove_user(&self, address: ManagedAddress) { + self.require_setup_period_live(); + require!( + !self.user_groups(&address).is_empty(), + "The address is not defined" + ); + let address_groups = self.user_groups(&address).get(); + for group_identifier in address_groups.iter() { + self.users_in_group(&group_identifier) + .update(|users_in_group_no| *users_in_group_no -= 1); + } + self.user_groups(&address).clear(); + self.claimed_balance(&address).clear(); + } + + //To change a receiving address, the user registers a request, which is afterwards accepted or not by the owner + #[endpoint(requestAddressChange)] + fn request_address_change(&self, new_address: ManagedAddress) { + self.require_setup_period_ended(); + let user_address = self.blockchain().get_caller(); + self.address_change_request(&user_address).set(&new_address); + } + + #[only_owner] + #[endpoint(approveAddressChange)] + fn approve_address_change(&self, user_address: ManagedAddress) { + self.require_setup_period_ended(); + require!( + !self.address_change_request(&user_address).is_empty(), + "The address does not have a change request" + ); + + // Get old address values + let new_address = self.address_change_request(&user_address).get(); + let user_current_groups = self.user_groups(&user_address).get(); + let user_claimed_balance = self.claimed_balance(&user_address).get(); + + // Save the new address with the old address values + self.user_groups(&new_address).set(&user_current_groups); + self.claimed_balance(&new_address) + .set(&user_claimed_balance); + + // Delete the old address + self.user_groups(&user_address).clear(); + self.claimed_balance(&user_address).clear(); + + // Delete the change request + self.address_change_request(&user_address).clear(); + } + + #[only_owner] + #[endpoint(endSetupPeriod)] + fn end_setup_period(&self) { + self.require_setup_period_live(); + let token_identifier = self.token_identifier().get(); + let total_mint_tokens = self.token_total_supply().get(); + self.mint_all_tokens(&token_identifier, &total_mint_tokens); + let activation_timestamp = self.blockchain().get_block_timestamp(); + self.activation_timestamp().set(activation_timestamp); + self.setup_period_status().set(false); + } + + #[endpoint(claimTokens)] + fn claim_tokens(&self) -> BigUint { + self.require_setup_period_ended(); + let token_identifier = self.token_identifier().get(); + let caller = self.blockchain().get_caller(); + let current_claimable_amount = self.get_claimable_tokens(&caller); + + require!( + current_claimable_amount > BigUint::zero(), + "This address cannot currently claim any more tokens" + ); + self.send_tokens(&token_identifier, &caller, ¤t_claimable_amount); + self.claimed_balance(&caller) + .update(|current_balance| *current_balance += ¤t_claimable_amount); + + current_claimable_amount + } + + // views + + #[view] + fn verify_address_change(&self, address: &ManagedAddress) -> ManagedAddress { + self.address_change_request(address).get() + } + + #[view] + fn get_claimable_tokens(&self, address: &ManagedAddress) -> BigUint { + let total_claimable_amount = self.calculate_claimable_tokens(address); + let current_balance = self.claimed_balance(address).get(); + if total_claimable_amount > current_balance { + total_claimable_amount - current_balance + } else { + BigUint::zero() + } + } + + // private functions + + fn calculate_claimable_tokens(&self, address: &ManagedAddress) -> BigUint { + let starting_timestamp = self.activation_timestamp().get(); + let current_timestamp = self.blockchain().get_block_timestamp(); + let address_groups = self.user_groups(address).get(); + + let mut claimable_amount = BigUint::zero(); + + // Compute the total claimable amount at the time of the request, for all of the user groups + for group_identifier in address_groups.iter() { + let schedule = self.group_schedule(&group_identifier).get(); + let users_in_group_no = self.users_in_group(&group_identifier).get(); + let time_passed = current_timestamp - starting_timestamp; + + match schedule.unlock_type { + UnlockType::FixedAmount { + period_unlock_amount, + release_period, + release_ticks, + } => { + let mut periods_passed = time_passed / release_period; + if periods_passed == 0 { + continue; + } + if periods_passed > release_ticks { + periods_passed = release_ticks; + } + claimable_amount += BigUint::from(periods_passed) * period_unlock_amount + / BigUint::from(users_in_group_no); + }, + UnlockType::Percentage { + period_unlock_percentage, + release_period, + release_ticks, + } => { + let mut periods_passed = time_passed / release_period; + if periods_passed == 0 { + continue; + } + if periods_passed > release_ticks { + periods_passed = release_ticks; + } + claimable_amount += BigUint::from(periods_passed) + * &schedule.group_total_amount + * (period_unlock_percentage as u64) + / PERCENTAGE_TOTAL + / BigUint::from(users_in_group_no); + }, + } + } + + claimable_amount + } + + fn send_tokens( + &self, + token_identifier: &TokenIdentifier, + address: &ManagedAddress, + amount: &BigUint, + ) { + self.tx() + .to(address) + .single_esdt(token_identifier, 0, amount) + .transfer(); + } + + fn mint_all_tokens(&self, token_identifier: &TokenIdentifier, amount: &BigUint) { + self.send().esdt_local_mint(token_identifier, 0, amount); + } + + fn require_setup_period_live(&self) { + require!(self.setup_period_status().get(), "Setup period has ended"); + } + + fn require_setup_period_ended(&self) { + require!( + !(self.setup_period_status().get()), + "Setup period is still active" + ); + } + + // storage + #[storage_mapper("activationTimestamp")] + fn activation_timestamp(&self) -> SingleValueMapper; + + #[view(getTokenIdentifier)] + #[storage_mapper("tokenIdentifier")] + fn token_identifier(&self) -> SingleValueMapper; + + #[view(getTokenTotalSupply)] + #[storage_mapper("tokenTotalSupply")] + fn token_total_supply(&self) -> SingleValueMapper; + + #[storage_mapper("setupPeriodStatus")] + fn setup_period_status(&self) -> SingleValueMapper; + + #[storage_mapper("addressChangeRequest")] + fn address_change_request(&self, address: &ManagedAddress) + -> SingleValueMapper; + + #[storage_mapper("groupSchedule")] + fn group_schedule( + &self, + group_identifier: &ManagedBuffer, + ) -> SingleValueMapper>; + + #[storage_mapper("userGroups")] + fn user_groups(&self, address: &ManagedAddress) + -> SingleValueMapper>; + + #[storage_mapper("usersInGroup")] + fn users_in_group(&self, group_identifier: &ManagedBuffer) -> SingleValueMapper; + + #[storage_mapper("claimedBalance")] + fn claimed_balance(&self, address: &ManagedAddress) -> SingleValueMapper; +} + + +CARGO.TOML: +[package] +name = "token-release" +version = "0.0.1" +authors = [ "you",] +edition = "2021" +publish = false + +[lib] +path = "src/token_release.rs" + +[dependencies.multiversx-sc] +version = "0.54.6" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" +path = "../../../framework/scenario" + + + +INTERACTOR FOLDER: None +//////////////////////// diff --git a/tools/git-scraper/src/fetch.rs b/tools/git-scraper/src/fetch.rs new file mode 100644 index 0000000000..c6ae7da8c9 --- /dev/null +++ b/tools/git-scraper/src/fetch.rs @@ -0,0 +1,80 @@ +use reqwest::blocking::Client; +use serde_json::Value; + +pub(crate) fn fetch_directory_listing(client: &Client, url: &str) -> reqwest::Result { + println!("Fetching directory listing from: {}", url); + let response = client + .get(url) + .header("Accept", "application/vnd.github.v3+json") + .send()?; + + println!("Got response with status: {}", response.status()); + + if !response.status().is_success() { + println!("Error response body: {}", response.text()?); + panic!("Failed to fetch directory listing"); + } + + let json = response.json()?; + println!("Successfully parsed JSON response"); + Ok(json) +} + +pub(crate) fn fetch_file_content( + client: &Client, + folder_url: &str, + file_name: &str, +) -> Option { + let folder_response: Value = client.get(folder_url).send().ok()?.json().ok()?; + + if let Some(entries) = folder_response.as_array() { + for entry in entries { + if let Some(name) = entry["name"].as_str() { + if name == file_name { + if let Some(download_url) = entry["download_url"].as_str() { + return client.get(download_url).send().ok()?.text().ok(); + } + } + } + } + } + None +} + +pub(crate) fn fetch_directory_contents( + client: &Client, + folder_url: &str, + subfolder: &str, +) -> Option> { + let folder_response: Value = client.get(folder_url).send().ok()?.json().ok()?; + + if let Some(entries) = folder_response.as_array() { + for entry in entries { + if let Some(name) = entry["name"].as_str() { + if name == subfolder { + if let Some(subfolder_url) = entry["url"].as_str() { + let subfolder_response: Value = + client.get(subfolder_url).send().ok()?.json().ok()?; + if let Some(files) = subfolder_response.as_array() { + let mut results = Vec::new(); + for file_entry in files { + if let (Some(file_name), Some(download_url)) = ( + file_entry["name"].as_str(), + file_entry["download_url"].as_str(), + ) { + if let Ok(content) = + client.get(download_url).send().unwrap().text() + { + results.push((file_name.to_string(), content)); + } + } + } + return Some(results); + } + } + } + } + } + } + None +} diff --git a/tools/git-scraper/src/init.rs b/tools/git-scraper/src/init.rs new file mode 100644 index 0000000000..a1f7dac9ef --- /dev/null +++ b/tools/git-scraper/src/init.rs @@ -0,0 +1,46 @@ +use reqwest::blocking::Client; +use serde::Deserialize; +use std::fs; +use std::fs::File; +use std::io::{self, BufWriter}; +use toml; + +#[derive(Deserialize)] +struct Config { + github_token: String, +} + +const CONFIG_PATH: &str = "tools/git-scraper/config.toml"; + +pub(crate) fn create_client() -> Client { + let config_content = fs::read_to_string(CONFIG_PATH).expect("Failed to read config.toml"); + + let config: Config = toml::from_str(&config_content).expect("Failed to parse config.toml"); + + println!( + "Creating client with token: {}...", + &config.github_token[..10] + ); // Print first 10 chars of token + + Client::builder() + .user_agent("Rust GitHub Scraper") + .default_headers({ + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", config.github_token).parse().unwrap(), + ); + headers.insert( + reqwest::header::ACCEPT, + "application/vnd.github.v3+json".parse().unwrap(), + ); + headers + }) + .build() + .expect("Failed to create HTTP client") +} + +pub(crate) fn initialize_writer(file_path: &str) -> io::Result> { + let output_file = File::create(file_path)?; + Ok(BufWriter::new(output_file)) +} diff --git a/tools/git-scraper/src/scraper.rs b/tools/git-scraper/src/scraper.rs new file mode 100644 index 0000000000..4d9615129e --- /dev/null +++ b/tools/git-scraper/src/scraper.rs @@ -0,0 +1,62 @@ +use fetch::fetch_directory_listing; +use init::{create_client, initialize_writer}; +use reqwest::blocking::Client; +use serde_json::Value; +use std::fs::File; +use std::io::{self, BufWriter, Write}; +use write::{write_cargo_toml, write_interactor_files, write_readme, write_src_folder}; + +mod fetch; +mod init; +mod write; + +const GITHUB_API_URL: &str = + "https://api.github.com/repos/multiversx/mx-sdk-rs/contents/contracts/examples"; +const FILE_PATH: &str = "tools/git-scraper/contracts_dump.txt"; + +fn main() -> io::Result<()> { + let client = create_client(); + let mut writer = initialize_writer(FILE_PATH)?; + + let response = fetch_directory_listing(&client, GITHUB_API_URL).unwrap(); + if let Some(entries) = response.as_array() { + for entry in entries { + process_entry(&client, entry, &mut writer)?; + } + } + + writeln!(writer, "////////////////////////")?; + writer.flush()?; + println!("Contracts processed and saved to contracts_dump.txt"); + Ok(()) +} + +fn process_entry(client: &Client, entry: &Value, writer: &mut BufWriter) -> io::Result<()> { + if let Some(folder_name) = entry["name"].as_str() { + println!("Starting to process entry: {}", folder_name); + + if let Some(folder_url) = entry["url"].as_str() { + println!("Found URL: {}", folder_url); + + writeln!(writer, "////////////////////////")?; + writeln!(writer, "NAME: {}", folder_name)?; + println!("Processing contract {}", folder_name); + + println!("Fetching README..."); + write_readme(client, folder_url, writer, folder_name)?; + + println!("Fetching src folder..."); + write_src_folder(client, folder_url, writer, folder_name)?; + + println!("Fetching Cargo.toml..."); + write_cargo_toml(client, folder_url, writer, folder_name)?; + + println!("Fetching interactor files..."); + write_interactor_files(client, folder_url, writer, folder_name)?; + + writer.flush()?; + println!("Finished processing {}", folder_name); + } + } + Ok(()) +} diff --git a/tools/git-scraper/src/write.rs b/tools/git-scraper/src/write.rs new file mode 100644 index 0000000000..aa24e8ada0 --- /dev/null +++ b/tools/git-scraper/src/write.rs @@ -0,0 +1,84 @@ +use crate::fetch::{fetch_directory_contents, fetch_file_content}; +use reqwest::blocking::Client; +use std::fs::File; +use std::io::{self, BufWriter, Write}; + +pub(crate) fn write_readme( + client: &Client, + folder_url: &str, + writer: &mut BufWriter, + folder_name: &str, +) -> io::Result<()> { + if let Some(readme_content) = fetch_file_content(client, folder_url, "README.md") { + writeln!(writer, "\nDESCRIPTION:\n{}", readme_content)?; + } else { + writeln!(writer, "\nDESCRIPTION:\nNone")?; + println!("No README.md available for {}", folder_name); + } + writer.flush()?; + Ok(()) +} + +pub(crate) fn write_src_folder( + client: &Client, + folder_url: &str, + writer: &mut BufWriter, + folder_name: &str, +) -> io::Result<()> { + writeln!(writer, "\nSRC FOLDER:")?; + if let Some(src_files) = fetch_directory_contents(client, folder_url, "src") { + for (file_name, file_content) in src_files { + writeln!(writer, "FILE_NAME: {}", file_name)?; + writeln!(writer, "{}", file_content)?; + writer.flush()?; + } + } else { + println!("No src folder found for {}", folder_name); + } + writer.flush()?; + Ok(()) +} + +pub(crate) fn write_cargo_toml( + client: &Client, + folder_url: &str, + writer: &mut BufWriter, + folder_name: &str, +) -> io::Result<()> { + if let Some(cargo_content) = fetch_file_content(client, folder_url, "Cargo.toml") { + writeln!(writer, "\nCARGO.TOML:")?; + writeln!(writer, "{}", cargo_content)?; + } else { + println!("No Cargo.toml found for {}", folder_name); + } + writer.flush()?; + Ok(()) +} + +pub(crate) fn write_interactor_files( + client: &Client, + folder_url: &str, + writer: &mut BufWriter, + folder_name: &str, +) -> io::Result<()> { + if let Some(interactor_files) = fetch_directory_contents(client, folder_url, "interactor/src") { + writeln!(writer, "\nINTERACTOR FOLDER:")?; + for (file_name, file_content) in interactor_files { + writeln!(writer, "FILE_NAME: {}", file_name)?; + writeln!(writer, "{}", file_content)?; + writer.flush()?; + } + } else { + writeln!(writer, "\nINTERACTOR FOLDER: None")?; + println!("No interactor/src folder found for {}", folder_name); + } + + if let Some(interactor_cargo_content) = fetch_file_content(client, folder_url, "interactor/Cargo.toml") { + writeln!(writer, "\nINTERACTOR CARGO.TOML:")?; + writeln!(writer, "{}", interactor_cargo_content)?; + } else { + println!("No interactor Cargo.toml found for {}", folder_name); + } + writer.flush()?; + Ok(()) +} From a1194865e391cc1849d570e7dcffed82ce440948 Mon Sep 17 00:00:00 2001 From: Mihai Calin Luca Date: Mon, 23 Dec 2024 17:41:59 +0100 Subject: [PATCH 2/6] bugfixing, interactor, rate-limiting, docs --- tools/git-scraper/contracts_dump.txt | 978 ++++++++++++++++++++++++++- tools/git-scraper/readme.md | 46 ++ tools/git-scraper/src/fetch.rs | 106 ++- tools/git-scraper/src/scraper.rs | 23 +- tools/git-scraper/src/write.rs | 29 +- 5 files changed, 1114 insertions(+), 68 deletions(-) create mode 100644 tools/git-scraper/readme.md diff --git a/tools/git-scraper/contracts_dump.txt b/tools/git-scraper/contracts_dump.txt index 61afcfd4dd..a1ed2d5833 100644 --- a/tools/git-scraper/contracts_dump.txt +++ b/tools/git-scraper/contracts_dump.txt @@ -176,7 +176,335 @@ path = "../../../framework/base" version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: +FILE_NAME: basic_interactor.rs +mod basic_interactor_cli; +mod basic_interactor_config; +mod basic_interactor_state; + +use adder::adder_proxy; +pub use basic_interactor_config::Config; +use basic_interactor_state::State; +use clap::Parser; + +use multiversx_sc_snippets::imports::*; + +const ADDER_CODE_PATH: MxscPath = MxscPath::new("../output/adder.mxsc.json"); + +pub async fn adder_cli() { + env_logger::init(); + + let config = Config::load_config(); + + let mut basic_interact = AdderInteract::new(config).await; + + let cli = basic_interactor_cli::InteractCli::parse(); + match &cli.command { + Some(basic_interactor_cli::InteractCliCommand::Deploy) => { + basic_interact.deploy().await; + }, + Some(basic_interactor_cli::InteractCliCommand::Upgrade(args)) => { + let owner_address = basic_interact.adder_owner_address.clone(); + basic_interact + .upgrade(args.value, &owner_address, None) + .await + }, + Some(basic_interactor_cli::InteractCliCommand::Add(args)) => { + basic_interact.add(args.value).await; + }, + Some(basic_interactor_cli::InteractCliCommand::Sum) => { + let sum = basic_interact.get_sum().await; + println!("sum: {sum}"); + }, + None => {}, + } +} + +pub struct AdderInteract { + pub interactor: Interactor, + pub adder_owner_address: Bech32Address, + pub wallet_address: Bech32Address, + pub state: State, +} + +impl AdderInteract { + 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/adder/interactor"); + + let adder_owner_address = interactor.register_wallet(test_wallets::heidi()).await; + let wallet_address = interactor.register_wallet(test_wallets::ivan()).await; + + interactor.generate_blocks_until_epoch(1).await.unwrap(); + + AdderInteract { + interactor, + adder_owner_address: adder_owner_address.into(), + wallet_address: wallet_address.into(), + state: State::load_state(), + } + } + + pub async fn deploy(&mut self) { + let new_address = self + .interactor + .tx() + .from(&self.adder_owner_address.clone()) + .gas(6_000_000) + .typed(adder_proxy::AdderProxy) + .init(0u64) + .code(ADDER_CODE_PATH) + .returns(ReturnsNewBech32Address) + .run() + .await; + + println!("new address: {new_address}"); + self.state.set_adder_address(new_address); + } + + pub async fn upgrade(&mut self, new_value: u32, sender: &Bech32Address, err: Option<&str>) { + let response = self + .interactor + .tx() + .from(sender) + .to(self.state.current_adder_address()) + .gas(6_000_000) + .typed(adder_proxy::AdderProxy) + .upgrade(new_value) + .code(ADDER_CODE_PATH) + .code_metadata(CodeMetadata::UPGRADEABLE) + .returns(ReturnsHandledOrError::new()) + .run() + .await; + + match response { + Ok(_) => { + println!("Contract successfully upgraded."); + }, + Err(tx_err) => { + println!("Contract failed upgrade with error: {}", tx_err.message); + assert_eq!(tx_err.message, err.unwrap_or_default()); + }, + } + } + + pub async fn add(&mut self, value: u32) { + self.interactor + .tx() + .from(&self.wallet_address) + .to(self.state.current_adder_address()) + .gas(6_000_000u64) + .typed(adder_proxy::AdderProxy) + .add(value) + .run() + .await; + + println!("Successfully performed add"); + } + + pub async fn get_sum(&mut self) -> RustBigUint { + self.interactor + .query() + .to(self.state.current_adder_address()) + .typed(adder_proxy::AdderProxy) + .sum() + .returns(ReturnsResultUnmanaged) + .run() + .await + } +} + +FILE_NAME: basic_interactor_cli.rs +use clap::{Args, Parser, Subcommand}; + +/// Adder Interact CLI +#[derive(Default, PartialEq, Eq, Debug, Parser)] +#[command(version, about)] +#[command(propagate_version = true)] +pub struct InteractCli { + #[command(subcommand)] + pub command: Option, +} + +/// Adder Interact CLI Commands +#[derive(Clone, PartialEq, Eq, Debug, Subcommand)] +pub enum InteractCliCommand { + #[command(name = "deploy", about = "Deploy contract")] + Deploy, + #[command(name = "upgrade", about = "Upgrade contract")] + Upgrade(UpgradeArgs), + #[command(name = "sum", about = "Print sum")] + Sum, + #[command(name = "add", about = "Add value")] + Add(AddArgs), +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct AddArgs { + /// The value to add + #[arg(short = 'v', long = "value")] + pub value: u32, +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct UpgradeArgs { + /// The value to add + #[arg(short = 'v', long = "value")] + pub value: u32, +} + +#[derive(Default, Clone, PartialEq, Eq, Debug, Args)] +pub struct MultiDeployArgs { + /// The number of contracts to deploy + #[arg(short = 'c', long = "count")] + pub count: usize, +} + +FILE_NAME: basic_interactor_config.rs +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, +} + +/// Adder 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, + } + } +} + +FILE_NAME: basic_interactor_main.rs +extern crate basic_interactor; + +#[tokio::main] +pub async fn main() { + basic_interactor::adder_cli().await; +} + +FILE_NAME: basic_interactor_state.rs +use multiversx_sc_snippets::imports::*; +use serde::{Deserialize, Serialize}; +use std::{ + io::{Read, Write}, + path::Path, +}; + +/// State file +const STATE_FILE: &str = "state.toml"; + +/// Adder Interact state +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct State { + adder_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 adder address + pub fn set_adder_address(&mut self, address: Bech32Address) { + self.adder_address = Some(address); + } + + /// Returns the adder contract + pub fn current_adder_address(&self) -> &Bech32Address { + self.adder_address + .as_ref() + .expect("no known adder 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(); + } +} + + +INTERACTOR CARGO.TOML: +[package] +name = "basic-interactor" +version = "0.0.0" +authors = ["MultiversX "] +edition = "2021" +publish = false + +[[bin]] +name = "basic-interactor" +path = "src/basic_interactor_main.rs" + +[lib] +path = "src/basic_interactor.rs" + +[dependencies.adder] +path = ".." + +[dependencies.multiversx-sc-snippets] +version = "0.54.6" +path = "../../../../framework/snippets" + +[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 = [] + //////////////////////// NAME: bonding-curve-contract @@ -371,7 +699,7 @@ path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: check-pause @@ -426,7 +754,7 @@ path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: crowdfunding-esdt @@ -749,7 +1077,7 @@ num-traits = "0.2" hex = "0.4" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: crypto-bubbles @@ -903,7 +1231,7 @@ version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: crypto-kitties @@ -911,8 +1239,9 @@ DESCRIPTION: None SRC FOLDER: +No src folder found -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: crypto-zombies @@ -1979,7 +2308,7 @@ version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: digital-cash @@ -2815,7 +3144,7 @@ version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: empty @@ -2863,7 +3192,7 @@ path = "../../../framework/scenario" num-bigint = "0.4" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: esdt-transfer-with-fee @@ -3056,7 +3385,7 @@ version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: factorial @@ -3117,7 +3446,7 @@ version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: fractional-nfts @@ -3393,7 +3722,7 @@ version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: lottery-esdt @@ -3846,7 +4175,7 @@ version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: multisig @@ -5558,7 +5887,7 @@ num-traits = "0.2" hex = "0.4" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: nft-minter @@ -5925,7 +6254,7 @@ version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: nft-storage-prepay @@ -6061,7 +6390,7 @@ version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: nft-subscription @@ -6181,7 +6510,7 @@ version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: order-book @@ -6189,8 +6518,9 @@ DESCRIPTION: None SRC FOLDER: +No src folder found -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: ping-pong-egld @@ -6784,7 +7114,609 @@ path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: +FILE_NAME: interact.rs +mod interact_cli; +mod interact_config; +mod interact_state; + +use crate::interact_state::State; +use clap::Parser; +pub use interact_config::Config; +use ping_pong_egld::proxy_ping_pong_egld::{self, ContractState, UserStatus}; + +use multiversx_sc_snippets::imports::*; + +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/examples/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 + } +} + +FILE_NAME: interact_cli.rs +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, +} + +FILE_NAME: interact_config.rs +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, + } + } +} + +FILE_NAME: interact_main.rs +extern crate ping_pong_egld_interact; + +#[tokio::main] +pub async fn main() { + ping_pong_egld_interact::ping_pong_egld_cli().await; +} + +FILE_NAME: interact_state.rs +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(); + } +} + + +INTERACTOR CARGO.TOML: +[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.6" +path = "../../../../framework/snippets" + +[features] +chain-simulator-tests = [] + //////////////////////// NAME: proxy-pause @@ -6949,7 +7881,7 @@ path = "../check-pause" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: rewards-distribution @@ -7885,7 +8817,7 @@ version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: seed-nft-minter @@ -8337,7 +9269,7 @@ version = "0.54.6" path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// NAME: token-release @@ -8766,5 +9698,5 @@ path = "../../../framework/scenario" -INTERACTOR FOLDER: None +INTERACTOR FOLDER: //////////////////////// diff --git a/tools/git-scraper/readme.md b/tools/git-scraper/readme.md new file mode 100644 index 0000000000..c5b7c6f6ed --- /dev/null +++ b/tools/git-scraper/readme.md @@ -0,0 +1,46 @@ +# Git scraping tool for MultiversX example contracts. + +## Overview + +The aim of this tool is to scrape the source code of example contracts from the MultiversX GitHub repository and save it to a file that serves as training data for AI models. + +## Features + +- Fetches contract source code from `src` directories +- Retrieves contract documentation (README.md) +- Captures contract configuration (Cargo.toml) +- Includes interactor code when available +- Formats output in a consistent, readable structure + +## Setup + +### GitHub Authentication + +1. Generate a new GitHub Token [here](https://github.com/settings/tokens) + - Select "Public Repositories (read-only)" access + - Set expiration to a value less than 365 days (mvx limit) + +### Config file + +2. Create a `config.toml` file in the git-scraper directory: + +```toml title=config.toml +github_token = "your_github_token" +``` + +## Usage + +Run the tool from the mx-sdk-rs root directory using: +```bash +cargo run --bin git-scraper +``` + +The paths are not yet relative, so running from the root is required. + +The tool will: +1. Fetch all example contracts +2. Process their contents +3. Save the data to `git-scraper/contracts_dump.txt` + +After the file is created, it can be imported into known AI agents for training. The agents should then be able to generate new contracts based on the examples and along with the interactor code. + diff --git a/tools/git-scraper/src/fetch.rs b/tools/git-scraper/src/fetch.rs index c6ae7da8c9..4c2ac7b2b2 100644 --- a/tools/git-scraper/src/fetch.rs +++ b/tools/git-scraper/src/fetch.rs @@ -7,14 +7,14 @@ pub(crate) fn fetch_directory_listing(client: &Client, url: &str) -> reqwest::Re .get(url) .header("Accept", "application/vnd.github.v3+json") .send()?; - + println!("Got response with status: {}", response.status()); - + if !response.status().is_success() { println!("Error response body: {}", response.text()?); panic!("Failed to fetch directory listing"); } - + let json = response.json()?; println!("Successfully parsed JSON response"); Ok(json) @@ -46,35 +46,103 @@ pub(crate) fn fetch_directory_contents( folder_url: &str, subfolder: &str, ) -> Option> { + println!( + "Fetching contents from {} in subfolder {}", + folder_url, subfolder + ); + let folder_response: Value = client.get(folder_url).send().ok()?.json().ok()?; if let Some(entries) = folder_response.as_array() { for entry in entries { if let Some(name) = entry["name"].as_str() { if name == subfolder { - if let Some(subfolder_url) = entry["url"].as_str() { - let subfolder_response: Value = - client.get(subfolder_url).send().ok()?.json().ok()?; - if let Some(files) = subfolder_response.as_array() { - let mut results = Vec::new(); - for file_entry in files { - if let (Some(file_name), Some(download_url)) = ( - file_entry["name"].as_str(), - file_entry["download_url"].as_str(), - ) { - if let Ok(content) = - client.get(download_url).send().unwrap().text() - { - results.push((file_name.to_string(), content)); - } + if let Some(url) = entry["url"].as_str() { + println!("Found directory: {}", name); + return fetch_files_from_directory(client, url); + } + } + } + } + } + + println!("Directory {} not found", subfolder); + None +} + +pub(crate) fn fetch_interactor_contents( + client: &Client, + folder_url: &str, +) -> Option<(Vec<(String, String)>, Option)> { + println!("Fetching interactor contents from {}", folder_url); + + let folder_response: Value = client.get(folder_url).send().ok()?.json().ok()?; + + if let Some(entries) = folder_response.as_array() { + let mut src_contents = None; + let mut cargo_contents = None; + + for entry in entries { + if let Some(name) = entry["name"].as_str() { + if name == "interactor" { + if let Some(url) = entry["url"].as_str() { + println!("Found interactor directory"); + let interactor_response: Value = + client.get(url).send().ok()?.json().ok()?; + + if let Some(interactor_entries) = interactor_response.as_array() { + for interactor_entry in interactor_entries { + match interactor_entry["name"].as_str() { + Some("src") => { + if let Some(src_url) = interactor_entry["url"].as_str() { + src_contents = + fetch_files_from_directory(client, src_url); + } + }, + Some("Cargo.toml") => { + if let Some(download_url) = + interactor_entry["download_url"].as_str() + { + if let Ok(content) = + client.get(download_url).send().unwrap().text() + { + cargo_contents = Some(content); + } + } + }, + _ => {}, } } - return Some(results); } } } } } + + return Some((src_contents.unwrap_or_default(), cargo_contents)); + } + + None +} + +fn fetch_files_from_directory(client: &Client, url: &str) -> Option> { + println!("Fetching files from {}", url); + let response: Value = client.get(url).send().ok()?.json().ok()?; + + if let Some(files) = response.as_array() { + let mut results = Vec::new(); + for file_entry in files { + if let (Some(file_name), Some(download_url)) = ( + file_entry["name"].as_str(), + file_entry["download_url"].as_str(), + ) { + println!("Fetching file: {}", file_name); + if let Ok(content) = client.get(download_url).send().unwrap().text() { + results.push((file_name.to_string(), content)); + } + } + } + return Some(results); } None } diff --git a/tools/git-scraper/src/scraper.rs b/tools/git-scraper/src/scraper.rs index 4d9615129e..5d0871a170 100644 --- a/tools/git-scraper/src/scraper.rs +++ b/tools/git-scraper/src/scraper.rs @@ -4,6 +4,7 @@ use reqwest::blocking::Client; use serde_json::Value; use std::fs::File; use std::io::{self, BufWriter, Write}; +use std::{thread, time::Duration}; use write::{write_cargo_toml, write_interactor_files, write_readme, write_src_folder}; mod fetch; @@ -34,26 +35,26 @@ fn main() -> io::Result<()> { fn process_entry(client: &Client, entry: &Value, writer: &mut BufWriter) -> io::Result<()> { if let Some(folder_name) = entry["name"].as_str() { println!("Starting to process entry: {}", folder_name); - + if let Some(folder_url) = entry["url"].as_str() { println!("Found URL: {}", folder_url); - + writeln!(writer, "////////////////////////")?; writeln!(writer, "NAME: {}", folder_name)?; - println!("Processing contract {}", folder_name); - println!("Fetching README..."); + thread::sleep(Duration::from_millis(100)); + write_readme(client, folder_url, writer, folder_name)?; - - println!("Fetching src folder..."); + thread::sleep(Duration::from_millis(100)); + write_src_folder(client, folder_url, writer, folder_name)?; - - println!("Fetching Cargo.toml..."); + thread::sleep(Duration::from_millis(100)); + write_cargo_toml(client, folder_url, writer, folder_name)?; - - println!("Fetching interactor files..."); + thread::sleep(Duration::from_millis(100)); + write_interactor_files(client, folder_url, writer, folder_name)?; - + writer.flush()?; println!("Finished processing {}", folder_name); } diff --git a/tools/git-scraper/src/write.rs b/tools/git-scraper/src/write.rs index aa24e8ada0..a3a25a7b61 100644 --- a/tools/git-scraper/src/write.rs +++ b/tools/git-scraper/src/write.rs @@ -1,4 +1,4 @@ -use crate::fetch::{fetch_directory_contents, fetch_file_content}; +use crate::fetch::{fetch_directory_contents, fetch_file_content, fetch_interactor_contents}; use reqwest::blocking::Client; use std::fs::File; use std::io::{self, BufWriter, Write}; @@ -30,9 +30,9 @@ pub(crate) fn write_src_folder( for (file_name, file_content) in src_files { writeln!(writer, "FILE_NAME: {}", file_name)?; writeln!(writer, "{}", file_content)?; - writer.flush()?; } } else { + writeln!(writer, "No src folder found")?; println!("No src folder found for {}", folder_name); } writer.flush()?; @@ -61,23 +61,22 @@ pub(crate) fn write_interactor_files( writer: &mut BufWriter, folder_name: &str, ) -> io::Result<()> { - if let Some(interactor_files) = fetch_directory_contents(client, folder_url, "interactor/src") { + if let Some((src_files, cargo_content)) = fetch_interactor_contents(client, folder_url) { writeln!(writer, "\nINTERACTOR FOLDER:")?; - for (file_name, file_content) in interactor_files { - writeln!(writer, "FILE_NAME: {}", file_name)?; - writeln!(writer, "{}", file_content)?; - writer.flush()?; + + if !src_files.is_empty() { + for (file_name, file_content) in src_files { + writeln!(writer, "FILE_NAME: {}", file_name)?; + writeln!(writer, "{}", file_content)?; + } } - } else { - writeln!(writer, "\nINTERACTOR FOLDER: None")?; - println!("No interactor/src folder found for {}", folder_name); - } - if let Some(interactor_cargo_content) = fetch_file_content(client, folder_url, "interactor/Cargo.toml") { - writeln!(writer, "\nINTERACTOR CARGO.TOML:")?; - writeln!(writer, "{}", interactor_cargo_content)?; + if let Some(cargo_content) = cargo_content { + writeln!(writer, "\nINTERACTOR CARGO.TOML:")?; + writeln!(writer, "{}", cargo_content)?; + } } else { - println!("No interactor Cargo.toml found for {}", folder_name); + println!("No interactor folder found for {}", folder_name); } writer.flush()?; Ok(()) From c09b5d57ac99b9cbbdae7549db4b37d0f08bd1be Mon Sep 17 00:00:00 2001 From: Mihai Calin Luca Date: Thu, 9 Jan 2025 12:15:45 +0200 Subject: [PATCH 3/6] instructions --- tools/git-scraper/contracts_dump.txt | 338 ++++++++++++++++++--------- tools/git-scraper/src/scraper.rs | 4 +- tools/git-scraper/src/write.rs | 101 ++++++++ 3 files changed, 332 insertions(+), 111 deletions(-) diff --git a/tools/git-scraper/contracts_dump.txt b/tools/git-scraper/contracts_dump.txt index a1ed2d5833..0301fd769e 100644 --- a/tools/git-scraper/contracts_dump.txt +++ b/tools/git-scraper/contracts_dump.txt @@ -1,3 +1,97 @@ +INSTRUCTIONS FOR USING THIS FILE +============================== +1. Each contract is separated by '////////////////////////'. The end of this section is also marked by '////////////////////////'. +2. For each contract you will find: + - NAME: The contract's folder name + - DESCRIPTION: Content from README.md + - SRC FOLDER: All source files + - CARGO.TOML: Dependencies and contract configuration + - INTERACTOR FOLDER: If available, contains interactor files (used for deployment and interaction on the blockchain) +3. Before the contract code dump you will find a step by step description of how to create, build and deploy smart contracts on MultiversX + +INSTRUCTIONS FOR CREATING, BUILDING AND DEPLOYING SMART CONTRACTS ON MULTIVERSX +============================== +1. Considering environment, the only critical components a developer should install are: +- rust (using rustup for better version management, as recommended on rust-lang.org): +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` +After installing rust, use the `stable` toolchain: +```bash +rustup update +rustup default stable +``` +- sc-meta tool: +```bash +cargo install multiversx-sc-meta --locked +``` +Once sc-meta is ready, install the wasm32 target (for the Rust compiler), wasm-opt, and others dependencies as follows: +```bash +# Installs `wasm32`, `wasm-opt`, and others in one go: +sc-meta install all +cargo install twiggy +``` +If the dependencies installation fails (sc-meta install all) use `sc-meta install [dependency-name]` to install dependencies one by one. + +2. In order to start writing a smart contract from an empty folder, the easiest way is to use the sc-meta template feature with the `new` command: +```bash +sc-meta new --template empty --name my-contract +``` +This will create a new project with the following structure: +- src/ + - lib.rs (main contract file, must include #[multiversx_sc::contract] attribute) +- wasm/ +- Cargo.toml + +3. After creating a template, a developer should start writing rust code in src/. src/lib.rs is the main file, you can also create other rust files +as modules marked with #[multiversx_sc::module] and import them in the main file). + +Key requirements: +- Contract must have #[init] function for deployment +- Public functions need #[endpoint] attribute +- Storage mappers (like SingleValueMapper, UnorderedSetMapper) need #[storage_mapper] and #[view] for easier access API +- Use MultiversX SC types (BigUint, ManagedBuffer, etc.) +- Split code into modules using #[multiversx_sc::module] for better organization + +4. After the code is written, it should first compile. A quick `cargo check` can verify the compilation. + +5. If the code compiles, it is time to build the contract. A contract should build without errors (and preferably warnings): +```bash +sc-meta all build +``` +This will generate: +- wasm/my-contract.wasm (the contract bytecode) +- wasm/my-contract.mxsc.json (contract metadata) + +6. After the build is done, we can use the interactor to deploy the contract. Generate it with: +```bash +sc-meta all snippets +``` +The interactor allows you to: +- Configure your wallet (use wallets from test_wallet for easier devnet deployment) +- Choose network (devnet/testnet/mainnet) +- Set gas limits +- Send deploy/upgrade transactions for your contract through Rust functions +- Call contract endpoints with arguments through Rust functions + +In short: +- env installation (rust and sc-meta) +- sc-meta new --template empty --name my-contract (new contract from template) +- write rust code inside src/ (remember required attributes) +- cargo check +- sc-meta all build +- write interactor code/ generate using sc-meta all snippets +- deploy the contract on devnet using the interactor (recommended for testing, no real EGLD needed) + +Common issues: +- Missing contract/module attributes +- Incorrect types in function arguments +- Storage not properly initialized +- Gas limits too low +- Missing endpoint attributes + +//////////////////////// + //////////////////////// NAME: adder @@ -169,11 +263,11 @@ publish = false path = "src/adder.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -237,7 +331,7 @@ impl AdderInteract { let adder_owner_address = interactor.register_wallet(test_wallets::heidi()).await; let wallet_address = interactor.register_wallet(test_wallets::ivan()).await; - interactor.generate_blocks_until_epoch(1).await.unwrap(); + interactor.generate_blocks(30u64).await.unwrap(); AdderInteract { interactor, @@ -493,7 +587,7 @@ path = "src/basic_interactor.rs" path = ".." [dependencies.multiversx-sc-snippets] -version = "0.54.6" +version = "0.55.0" path = "../../../../framework/snippets" [dependencies] @@ -501,6 +595,7 @@ clap = { version = "4.4.7", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } toml = "0.8.6" tokio = { version = "1.24" } +serial_test = { version = "3.2.0" } [features] chain-simulator-tests = [] @@ -577,13 +672,13 @@ pub trait Contract: #[init] fn init(&self) {} - #[payable("*")] + #[payable] #[endpoint(sellToken)] fn sell_token_endpoint(&self) { self.sell_token::>(); } - #[payable("*")] + #[payable] #[endpoint(buyToken)] fn buy_token_endpoint( &self, @@ -599,7 +694,7 @@ pub trait Contract: } #[endpoint(deposit)] - #[payable("*")] + #[payable] fn deposit_endpoint(&self, payment_token: OptionalValue) { self.deposit::>(payment_token) } @@ -686,15 +781,15 @@ publish = false path = "src/bonding_curve_contract.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dependencies.multiversx-sc-modules] -version = "0.54.6" +version = "0.55.0" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -741,15 +836,15 @@ path = "src/check_pause.rs" num-bigint = "0.4" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dependencies.multiversx-sc-modules] -version = "0.54.6" +version = "0.55.0" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -794,7 +889,7 @@ pub trait Crowdfunding { } #[endpoint] - #[payable("*")] + #[payable] fn fund(&self) { let (token, _, payment) = self.call_value().egld_or_single_esdt().into_tuple(); @@ -1064,11 +1159,11 @@ publish = false path = "src/crowdfunding_esdt.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" [dev-dependencies] @@ -1101,7 +1196,7 @@ pub trait CryptoBubbles { #[payable("EGLD")] #[endpoint(topUp)] fn top_up(&self) { - let payment = self.call_value().egld_value(); + let payment = self.call_value().egld(); let caller = self.blockchain().get_caller(); self.player_balance(&caller) .update(|balance| *balance += &*payment); @@ -1151,7 +1246,7 @@ pub trait CryptoBubbles { #[payable("EGLD")] #[endpoint(joinGame)] fn join_game(&self, game_index: BigUint) { - let bet = self.call_value().egld_value(); + let bet = self.call_value().egld(); let player = self.blockchain().get_caller(); self.top_up(); self.add_player_to_game_state_change(&game_index, &player, &bet) @@ -1223,11 +1318,11 @@ publish = false path = "src/crypto_bubbles.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -2249,7 +2344,7 @@ pub trait ZombieHelper: storage::Storage { #[payable("EGLD")] #[endpoint] fn level_up(&self, zombie_id: usize) { - let payment_amount = self.call_value().egld_value(); + let payment_amount = self.call_value().egld(); let fee = self.level_up_fee().get(); require!(*payment_amount == fee, "Payment must be must be 0.001 EGLD"); self.zombies(&zombie_id) @@ -2300,11 +2395,11 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -2886,10 +2981,10 @@ use crate::{constants::*, helpers, storage}; #[multiversx_sc::module] pub trait PayFeeAndFund: storage::StorageModule + helpers::HelpersModule { #[endpoint(payFeeAndFundESDT)] - #[payable("*")] + #[payable] fn pay_fee_and_fund_esdt(&self, address: ManagedAddress, valability: u64) { - let mut payments = self.call_value().all_esdt_transfers().clone_value(); - let fee = EgldOrEsdtTokenPayment::from(payments.get(0)); + let mut payments = self.call_value().all_esdt_transfers().clone(); + let fee = EgldOrEsdtTokenPayment::from(payments.get(0).clone()); let caller_address = self.blockchain().get_caller(); self.update_fees(caller_address, &address, fee); @@ -2900,7 +2995,7 @@ pub trait PayFeeAndFund: storage::StorageModule + helpers::HelpersModule { #[endpoint(payFeeAndFundEGLD)] #[payable("EGLD")] fn pay_fee_and_fund_egld(&self, address: ManagedAddress, valability: u64) { - let mut fund = self.call_value().egld_value().clone_value(); + let mut fund = self.call_value().egld().clone(); let fee_value = self.fee(&EgldOrEsdtTokenIdentifier::egld()).get(); require!(fund > fee_value, "payment not covering fees"); @@ -2913,7 +3008,7 @@ pub trait PayFeeAndFund: storage::StorageModule + helpers::HelpersModule { } #[endpoint] - #[payable("*")] + #[payable] fn fund(&self, address: ManagedAddress, valability: u64) { require!(!self.deposit(&address).is_empty(), FEES_NOT_COVERED_ERR_MSG); let deposit_mapper = self.deposit(&address).get(); @@ -2924,8 +3019,9 @@ pub trait PayFeeAndFund: storage::StorageModule + helpers::HelpersModule { ); let deposited_fee_token = deposit_mapper.fees.value; let fee_amount = self.fee(&deposited_fee_token.token_identifier).get(); - let egld_payment = self.call_value().egld_value().clone_value(); - let esdt_payment = self.call_value().all_esdt_transfers().clone_value(); + // TODO: switch to egld+esdt multi transfer handling + let egld_payment = self.call_value().egld_direct_non_strict().clone(); + let esdt_payment = self.call_value().all_esdt_transfers().clone(); let num_tokens = self.get_num_token_transfers(&egld_payment, &esdt_payment); self.check_fees_cover_number_of_tokens(num_tokens, fee_amount, deposited_fee_token.amount); @@ -3037,7 +3133,7 @@ pub trait SignatureOperationsModule: storage::StorageModule + helpers::HelpersMo } #[endpoint] - #[payable("*")] + #[payable] fn forward( &self, address: ManagedAddress, @@ -3136,11 +3232,11 @@ publish = false path = "src/digital_cash.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -3181,11 +3277,11 @@ publish = false path = "src/empty.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" [dev-dependencies] @@ -3259,11 +3355,11 @@ pub trait EsdtTransferWithFee { self.tx().to(ToCaller).payment(fees).transfer(); } - #[payable("*")] + #[payable] #[endpoint] fn transfer(&self, address: ManagedAddress) { require!( - *self.call_value().egld_value() == 0, + *self.call_value().egld_direct_non_strict() == 0, "EGLD transfers not allowed" ); let payments = self.call_value().all_esdt_transfers(); @@ -3287,13 +3383,13 @@ pub trait EsdtTransferWithFee { "Mismatching payment for covering fees" ); let _ = self.get_payment_after_fees(fee_type, &next_payment); - new_payments.push(payment); + new_payments.push(payment.clone()); }, Fee::Percentage(_) => { new_payments.push(self.get_payment_after_fees(fee_type, &payment)); }, Fee::Unset => { - new_payments.push(payment); + new_payments.push(payment.clone()); }, } } @@ -3377,11 +3473,11 @@ publish = false path = "src/esdt_transfer_with_fee.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -3438,11 +3534,11 @@ publish = false path = "src/factorial.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -3477,10 +3573,10 @@ pub trait FractionalNfts: default_issue_callbacks::DefaultIssueCallbacksModule { token_ticker: ManagedBuffer, num_decimals: usize, ) { - let issue_cost = self.call_value().egld_value(); + let issue_cost = self.call_value().egld(); self.fractional_token().issue_and_set_all_roles( EsdtTokenType::SemiFungible, - issue_cost.clone_value(), + issue_cost.clone(), token_display_name, token_ticker, num_decimals, @@ -3504,7 +3600,7 @@ pub trait FractionalNfts: default_issue_callbacks::DefaultIssueCallbacksModule { .async_call_and_exit(); } - #[payable("*")] + #[payable] #[endpoint(fractionalizeNFT)] fn fractionalize_nft( &self, @@ -3531,7 +3627,7 @@ pub trait FractionalNfts: default_issue_callbacks::DefaultIssueCallbacksModule { let fractional_token = fractional_token_mapper.get_token_id_ref(); let hash = ManagedBuffer::new(); let fractional_info = - FractionalUriInfo::new(original_payment, initial_fractional_amount.clone()); + FractionalUriInfo::new(original_payment.clone(), initial_fractional_amount.clone()); let uris = fractional_info.to_uris(); let fractional_nonce = self.send().esdt_nft_create( @@ -3554,7 +3650,7 @@ pub trait FractionalNfts: default_issue_callbacks::DefaultIssueCallbacksModule { .transfer(); } - #[payable("*")] + #[payable] #[endpoint(unFractionalizeNFT)] fn unfractionalize_nft(&self) { let fractional_payment = self.call_value().single_esdt(); @@ -3710,15 +3806,15 @@ publish = false path = "src/fractional_nfts.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dependencies.multiversx-sc-modules] -version = "0.54.6" +version = "0.55.0" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -3881,7 +3977,7 @@ pub trait Lottery { if let Some(whitelist) = opt_whitelist.as_option() { let mut mapper = self.lottery_whitelist(&lottery_name); for addr in &*whitelist { - mapper.insert(addr); + mapper.insert(addr.clone()); } } @@ -3899,7 +3995,7 @@ pub trait Lottery { } #[endpoint] - #[payable("*")] + #[payable] fn buy_ticket(&self, lottery_name: ManagedBuffer) { let (token_identifier, payment) = self.call_value().egld_or_single_fungible_esdt(); @@ -4167,11 +4263,11 @@ publish = false path = "src/lottery.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -4390,7 +4486,7 @@ pub trait Multisig: } /// Allows the contract to receive funds even if it is marked as unpayable in the protocol. - #[payable("*")] + #[payable] #[endpoint] fn deposit(&self) {} @@ -5517,6 +5613,7 @@ pub trait MultisigStateModule { fn add_multiple_board_members(&self, new_board_members: ManagedVec) -> usize { let mut duplicates = false; + let new_board_members_len = new_board_members.len(); self.user_mapper().get_or_create_users( new_board_members.into_iter(), |user_id, new_user| { @@ -5529,7 +5626,7 @@ pub trait MultisigStateModule { require!(!duplicates, "duplicate board member"); let num_board_members_mapper = self.num_board_members(); - let new_num_board_members = num_board_members_mapper.get() + new_board_members.len(); + let new_num_board_members = num_board_members_mapper.get() + new_board_members_len; num_board_members_mapper.set(new_num_board_members); new_num_board_members @@ -5860,15 +5957,15 @@ publish = false path = "src/multisig.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dependencies.multiversx-sc-modules] -version = "0.54.6" +version = "0.55.0" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" [dev-dependencies.adder] @@ -5878,7 +5975,7 @@ path = "../adder" path = "../factorial" [dev-dependencies.multiversx-wegld-swap-sc] -version = "0.54.6" +version = "0.55.0" path = "../../core/wegld-swap" [dev-dependencies] @@ -6061,11 +6158,11 @@ pub trait NftModule { fn issue_token(&self, token_name: ManagedBuffer, token_ticker: ManagedBuffer) { require!(self.nft_token_id().is_empty(), "Token already issued"); - let payment_amount = self.call_value().egld_value(); + let payment_amount = self.call_value().egld(); self.send() .esdt_system_sc_proxy() .issue_non_fungible( - payment_amount.clone_value(), + payment_amount.clone(), &token_name, &token_ticker, NonFungibleTokenProperties { @@ -6099,7 +6196,7 @@ pub trait NftModule { // endpoints - #[payable("*")] + #[payable] #[endpoint(buyNft)] fn buy_nft(&self, nft_nonce: u64) { let payment = self.call_value().egld_or_single_esdt(); @@ -6246,11 +6343,11 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -6315,7 +6412,7 @@ pub trait NftStoragePrepay { #[payable("EGLD")] #[endpoint(depositPaymentForStorage)] fn deposit_payment_for_storage(&self) { - let payment = self.call_value().egld_value(); + let payment = self.call_value().egld(); let caller = self.blockchain().get_caller(); self.deposit(&caller) .update(|deposit| *deposit += &*payment); @@ -6382,11 +6479,11 @@ path = "src/nft_storage_prepay.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -6413,10 +6510,11 @@ pub trait NftSubscription: fn init(&self) {} #[endpoint] + #[payable("EGLD")] fn issue(&self) { self.token_id().issue_and_set_all_roles( EsdtTokenType::NonFungible, - self.call_value().egld_value().clone_value(), + self.call_value().egld().clone(), ManagedBuffer::from(b"Subscription"), ManagedBuffer::from(b"SUB"), 0, @@ -6447,37 +6545,57 @@ pub trait NftSubscription: .transfer(); } - #[payable("*")] + #[payable] #[endpoint] fn update_attributes(&self, attributes: ManagedBuffer) { - let (id, nonce, _) = self.call_value().single_esdt().into_tuple(); - self.update_subscription_attributes::(&id, nonce, attributes); + let payment = self.call_value().single_esdt(); + self.update_subscription_attributes::( + &payment.token_identifier, + payment.token_nonce, + attributes, + ); self.tx() .to(ToCaller) - .single_esdt(&id, nonce, &BigUint::from(1u8)) + .single_esdt( + &payment.token_identifier, + payment.token_nonce, + &BigUint::from(1u8), + ) .transfer(); } - #[payable("*")] + #[payable] #[endpoint] fn renew(&self, duration: u64) { - let (id, nonce, _) = self.call_value().single_esdt().into_tuple(); - self.renew_subscription::(&id, nonce, duration); + let payment = self.call_value().single_esdt(); + self.renew_subscription::( + &payment.token_identifier, + payment.token_nonce, + duration, + ); self.tx() .to(ToCaller) - .single_esdt(&id, nonce, &BigUint::from(1u8)) + .single_esdt( + &payment.token_identifier, + payment.token_nonce, + &BigUint::from(1u8), + ) .transfer(); } - #[payable("*")] + #[payable] #[endpoint] fn cancel(&self) { - let (id, nonce, _) = self.call_value().single_esdt().into_tuple(); - self.cancel_subscription::(&id, nonce); + let payment = self.call_value().single_esdt(); + self.cancel_subscription::(&payment.token_identifier, payment.token_nonce); self.tx() .to(ToCaller) - .single_esdt(&id, nonce, &BigUint::from(1u8)) + .single_esdt( + &payment.token_identifier, + payment.token_nonce, + &BigUint::from(1u8), + ) .transfer(); } @@ -6498,15 +6616,15 @@ publish = false path = "src/lib.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dependencies.multiversx-sc-modules] -version = "0.54.6" +version = "0.55.0" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -6603,7 +6721,7 @@ pub trait PingPong { #[payable("EGLD")] #[endpoint] fn ping(&self, _data: IgnoreValue) { - let payment = self.call_value().egld_value(); + let payment = self.call_value().egld(); require!( *payment == self.ping_amount().get(), @@ -7105,11 +7223,11 @@ publish = false path = "src/ping_pong.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -7711,7 +7829,7 @@ tokio = { version = "1.24" } path = ".." [dependencies.multiversx-sc-snippets] -version = "0.54.6" +version = "0.55.0" path = "../../../../framework/snippets" [features] @@ -7869,11 +7987,11 @@ publish = false path = "src/proxy_pause.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" [dev-dependencies.check-pause] @@ -7913,7 +8031,7 @@ pub struct Bracket { } #[type_abi] -#[derive(ManagedVecItem, NestedEncode, NestedDecode)] +#[derive(ManagedVecItem, NestedEncode, NestedDecode, Clone)] pub struct ComputedBracket { pub end_index: u64, pub nft_reward_percent: BigUint, @@ -7949,7 +8067,7 @@ pub trait RewardsDistribution: self.brackets().set(brackets); } - #[payable("*")] + #[payable] #[endpoint(depositRoyalties)] fn deposit_royalties(&self) { let payment = self.call_value().egld_or_single_esdt(); @@ -7966,7 +8084,7 @@ pub trait RewardsDistribution: .unwrap_or_else(|| self.new_raffle()); let mut rng = RandomnessSource::default(); - let mut bracket = raffle.computed_brackets.get(0); + let mut bracket = raffle.computed_brackets.get(0).clone(); let run_result = self.run_while_it_has_gas(DEFAULT_MIN_GAS_TO_SAVE_PROGRESS, || { let ticket = self.shuffle_and_pick_single_ticket( @@ -8030,7 +8148,7 @@ pub trait RewardsDistribution: ) { while ticket > bracket.end_index { computed_brackets.remove(0); - *bracket = computed_brackets.get(0); + *bracket = computed_brackets.get(0).clone(); } } @@ -8139,7 +8257,7 @@ pub trait RewardsDistribution: ); } - #[payable("*")] + #[payable] #[endpoint(claimRewards)] fn claim_rewards( &self, @@ -8805,15 +8923,15 @@ publish = false path = "src/rewards_distribution.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dependencies.multiversx-sc-modules] -version = "0.54.6" +version = "0.55.0" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -8863,7 +8981,7 @@ pub trait DistributionModule { } self.tx() .to(&distribution.address) - .raw_call(distribution.endpoint) + .raw_call(distribution.endpoint.clone()) .egld_or_single_esdt(token_id, token_nonce, &payment_amount) .gas(distribution.gas_limit) .transfer_execute(); @@ -8990,10 +9108,10 @@ pub trait NftModule: #[payable("EGLD")] #[endpoint(issueToken)] fn issue_token(&self, token_display_name: ManagedBuffer, token_ticker: ManagedBuffer) { - let issue_cost = self.call_value().egld_value(); + let issue_cost = self.call_value().egld(); self.nft_token_id().issue_and_set_all_roles( EsdtTokenType::NonFungible, - issue_cost.clone_value(), + issue_cost.clone(), token_display_name, token_ticker, 0, @@ -9003,7 +9121,7 @@ pub trait NftModule: // endpoints - #[payable("*")] + #[payable] #[endpoint(buyNft)] fn buy_nft(&self, nft_nonce: u64) { let payment = self.call_value().egld_or_single_esdt(); @@ -9151,7 +9269,7 @@ pub trait SeedNftMinter: marketplaces: ManagedVec, distribution: ManagedVec>, ) { - self.marketplaces().extend(&marketplaces); + self.marketplaces().extend(marketplaces); self.init_distribution(distribution); } @@ -9226,7 +9344,7 @@ pub trait SeedNftMinter: } else { esdt_payments .try_get(0) - .map(|esdt_payment| esdt_payment.amount) + .map(|esdt_payment| esdt_payment.amount.clone()) .unwrap_or_default() }; total_amount += amount; @@ -9257,15 +9375,15 @@ publish = false path = "src/seed_nft_minter.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dependencies.multiversx-sc-modules] -version = "0.54.6" +version = "0.55.0" path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" @@ -9689,11 +9807,11 @@ publish = false path = "src/token_release.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" path = "../../../framework/scenario" diff --git a/tools/git-scraper/src/scraper.rs b/tools/git-scraper/src/scraper.rs index 5d0871a170..8e34b6b056 100644 --- a/tools/git-scraper/src/scraper.rs +++ b/tools/git-scraper/src/scraper.rs @@ -5,7 +5,7 @@ use serde_json::Value; use std::fs::File; use std::io::{self, BufWriter, Write}; use std::{thread, time::Duration}; -use write::{write_cargo_toml, write_interactor_files, write_readme, write_src_folder}; +use write::{write_cargo_toml, write_instructions, write_interactor_files, write_readme, write_src_folder}; mod fetch; mod init; @@ -19,6 +19,8 @@ fn main() -> io::Result<()> { let client = create_client(); let mut writer = initialize_writer(FILE_PATH)?; + write_instructions(&mut writer)?; + let response = fetch_directory_listing(&client, GITHUB_API_URL).unwrap(); if let Some(entries) = response.as_array() { for entry in entries { diff --git a/tools/git-scraper/src/write.rs b/tools/git-scraper/src/write.rs index a3a25a7b61..80d80c5dd6 100644 --- a/tools/git-scraper/src/write.rs +++ b/tools/git-scraper/src/write.rs @@ -3,6 +3,107 @@ use reqwest::blocking::Client; use std::fs::File; use std::io::{self, BufWriter, Write}; +pub(crate) fn write_instructions(writer: &mut BufWriter) -> io::Result<()> { + let instructions = r#"INSTRUCTIONS FOR USING THIS FILE +============================== +1. Each contract is separated by '////////////////////////'. The end of this section is also marked by '////////////////////////'. +2. For each contract you will find: + - NAME: The contract's folder name + - DESCRIPTION: Content from README.md + - SRC FOLDER: All source files + - CARGO.TOML: Dependencies and contract configuration + - INTERACTOR FOLDER: If available, contains interactor files (used for deployment and interaction on the blockchain) +3. Before the contract code dump you will find a step by step description of how to create, build and deploy smart contracts on MultiversX + +INSTRUCTIONS FOR CREATING, BUILDING AND DEPLOYING SMART CONTRACTS ON MULTIVERSX +============================== +1. Considering environment, the only critical components a developer should install are: +- rust (using rustup for better version management, as recommended on rust-lang.org): +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` +After installing rust, use the `stable` toolchain: +```bash +rustup update +rustup default stable +``` +- sc-meta tool: +```bash +cargo install multiversx-sc-meta --locked +``` +Once sc-meta is ready, install the wasm32 target (for the Rust compiler), wasm-opt, and others dependencies as follows: +```bash +# Installs `wasm32`, `wasm-opt`, and others in one go: +sc-meta install all +cargo install twiggy +``` +If the dependencies installation fails (sc-meta install all) use `sc-meta install [dependency-name]` to install dependencies one by one. + +2. In order to start writing a smart contract from an empty folder, the easiest way is to use the sc-meta template feature with the `new` command: +```bash +sc-meta new --template empty --name my-contract +``` +This will create a new project with the following structure: +- src/ + - lib.rs (main contract file, must include #[multiversx_sc::contract] attribute) +- wasm/ +- Cargo.toml + +3. After creating a template, a developer should start writing rust code in src/. src/lib.rs is the main file, you can also create other rust files +as modules marked with #[multiversx_sc::module] and import them in the main file). + +Key requirements: +- Contract must have #[init] function for deployment +- Public functions need #[endpoint] attribute +- Storage mappers (like SingleValueMapper, UnorderedSetMapper) need #[storage_mapper] and #[view] for easier access API +- Use MultiversX SC types (BigUint, ManagedBuffer, etc.) +- Split code into modules using #[multiversx_sc::module] for better organization + +4. After the code is written, it should first compile. A quick `cargo check` can verify the compilation. + +5. If the code compiles, it is time to build the contract. A contract should build without errors (and preferably warnings): +```bash +sc-meta all build +``` +This will generate: +- wasm/my-contract.wasm (the contract bytecode) +- wasm/my-contract.mxsc.json (contract metadata) + +6. After the build is done, we can use the interactor to deploy the contract. Generate it with: +```bash +sc-meta all snippets +``` +The interactor allows you to: +- Configure your wallet (use wallets from test_wallet for easier devnet deployment) +- Choose network (devnet/testnet/mainnet) +- Set gas limits +- Send deploy/upgrade transactions for your contract through Rust functions +- Call contract endpoints with arguments through Rust functions + +In short: +- env installation (rust and sc-meta) +- sc-meta new --template empty --name my-contract (new contract from template) +- write rust code inside src/ (remember required attributes) +- cargo check +- sc-meta all build +- write interactor code/ generate using sc-meta all snippets +- deploy the contract on devnet using the interactor (recommended for testing, no real EGLD needed) + +Common issues: +- Missing contract/module attributes +- Incorrect types in function arguments +- Storage not properly initialized +- Gas limits too low +- Missing endpoint attributes + +//////////////////////// +"#; + + writeln!(writer, "{}", instructions)?; + writer.flush()?; + Ok(()) +} + pub(crate) fn write_readme( client: &Client, folder_url: &str, From cca176fad49e4d53ae613bada0127871285c3c24 Mon Sep 17 00:00:00 2001 From: Mihai Calin Luca Date: Thu, 9 Jan 2025 12:31:59 +0200 Subject: [PATCH 4/6] remove framework local paths --- tools/git-scraper/contracts_dump.txt | 54 +--------------------------- tools/git-scraper/src/scraper.rs | 8 ++++- tools/git-scraper/src/write.rs | 36 +++++++++++++++++++ 3 files changed, 44 insertions(+), 54 deletions(-) diff --git a/tools/git-scraper/contracts_dump.txt b/tools/git-scraper/contracts_dump.txt index 0301fd769e..3aecc4c4f7 100644 --- a/tools/git-scraper/contracts_dump.txt +++ b/tools/git-scraper/contracts_dump.txt @@ -264,11 +264,9 @@ path = "src/adder.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: FILE_NAME: basic_interactor.rs @@ -588,7 +586,6 @@ path = ".." [dependencies.multiversx-sc-snippets] version = "0.55.0" -path = "../../../../framework/snippets" [dependencies] clap = { version = "4.4.7", features = ["derive"] } @@ -782,15 +779,12 @@ path = "src/bonding_curve_contract.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dependencies.multiversx-sc-modules] version = "0.55.0" -path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" @@ -837,15 +831,12 @@ num-bigint = "0.4" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dependencies.multiversx-sc-modules] version = "0.55.0" -path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" @@ -1160,11 +1151,9 @@ path = "src/crowdfunding_esdt.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" [dev-dependencies] num-bigint = "0.4" @@ -1319,11 +1308,9 @@ path = "src/crypto_bubbles.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -2396,11 +2383,9 @@ path = "src/lib.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -3233,11 +3218,9 @@ path = "src/digital_cash.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -3278,11 +3261,9 @@ path = "src/empty.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" [dev-dependencies] num-bigint = "0.4" @@ -3474,11 +3455,9 @@ path = "src/esdt_transfer_with_fee.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -3535,11 +3514,9 @@ path = "src/factorial.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -3807,15 +3784,12 @@ path = "src/fractional_nfts.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dependencies.multiversx-sc-modules] version = "0.55.0" -path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -4264,11 +4238,9 @@ path = "src/lottery.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -5958,15 +5930,12 @@ path = "src/multisig.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dependencies.multiversx-sc-modules] version = "0.55.0" -path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" [dev-dependencies.adder] path = "../adder" @@ -5976,7 +5945,6 @@ path = "../factorial" [dev-dependencies.multiversx-wegld-swap-sc] version = "0.55.0" -path = "../../core/wegld-swap" [dev-dependencies] num-bigint = "0.4" @@ -6344,11 +6312,9 @@ path = "src/lib.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -6480,11 +6446,9 @@ path = "src/nft_storage_prepay.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -6617,15 +6581,12 @@ path = "src/lib.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dependencies.multiversx-sc-modules] version = "0.55.0" -path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -7224,11 +7185,9 @@ path = "src/ping_pong.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" @@ -7830,7 +7789,6 @@ path = ".." [dependencies.multiversx-sc-snippets] version = "0.55.0" -path = "../../../../framework/snippets" [features] chain-simulator-tests = [] @@ -7988,11 +7946,9 @@ path = "src/proxy_pause.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" [dev-dependencies.check-pause] path = "../check-pause" @@ -8924,15 +8880,12 @@ path = "src/rewards_distribution.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dependencies.multiversx-sc-modules] version = "0.55.0" -path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -9376,15 +9329,12 @@ path = "src/seed_nft_minter.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dependencies.multiversx-sc-modules] version = "0.55.0" -path = "../../../contracts/modules" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: @@ -9808,13 +9758,11 @@ path = "src/token_release.rs" [dependencies.multiversx-sc] version = "0.55.0" -path = "../../../framework/base" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" -path = "../../../framework/scenario" INTERACTOR FOLDER: -//////////////////////// +//////////////////////// \ No newline at end of file diff --git a/tools/git-scraper/src/scraper.rs b/tools/git-scraper/src/scraper.rs index 8e34b6b056..72cdca2ece 100644 --- a/tools/git-scraper/src/scraper.rs +++ b/tools/git-scraper/src/scraper.rs @@ -5,7 +5,10 @@ use serde_json::Value; use std::fs::File; use std::io::{self, BufWriter, Write}; use std::{thread, time::Duration}; -use write::{write_cargo_toml, write_instructions, write_interactor_files, write_readme, write_src_folder}; +use write::{ + cleanup_local_paths, write_cargo_toml, write_instructions, write_interactor_files, + write_readme, write_src_folder, +}; mod fetch; mod init; @@ -31,6 +34,9 @@ fn main() -> io::Result<()> { writeln!(writer, "////////////////////////")?; writer.flush()?; println!("Contracts processed and saved to contracts_dump.txt"); + + cleanup_local_paths(FILE_PATH)?; + Ok(()) } diff --git a/tools/git-scraper/src/write.rs b/tools/git-scraper/src/write.rs index 80d80c5dd6..75ea6f2fa1 100644 --- a/tools/git-scraper/src/write.rs +++ b/tools/git-scraper/src/write.rs @@ -1,5 +1,6 @@ use crate::fetch::{fetch_directory_contents, fetch_file_content, fetch_interactor_contents}; use reqwest::blocking::Client; +use std::fs; use std::fs::File; use std::io::{self, BufWriter, Write}; @@ -182,3 +183,38 @@ pub(crate) fn write_interactor_files( writer.flush()?; Ok(()) } + +pub(crate) fn cleanup_local_paths(file_path: &str) -> io::Result<()> { + let content = fs::read_to_string(file_path)?; + let lines: Vec<&str> = content.lines().collect(); + let mut result = Vec::new(); + let mut skip_next = false; + + for line in &lines { + if skip_next { + skip_next = false; + continue; + } + + if line.trim().starts_with("path = \"../../../") { + continue; + } + + if line.trim().starts_with("version = ") { + skip_next = has_local_path_next(&lines, line); + } + + result.push(*line); + } + + fs::write(file_path, result.join("\n"))?; + Ok(()) +} + +fn has_local_path_next<'a>(lines: &[&'a str], current: &str) -> bool { + lines + .iter() + .skip_while(|&&x| x != current) + .nth(1) + .map_or(false, |next| next.trim().starts_with("path = \"../../../")) +} From a8005323e16abedf8d08ae00cdee693894a8dc03 Mon Sep 17 00:00:00 2001 From: Mihai Calin Luca Date: Thu, 9 Jan 2025 12:42:12 +0200 Subject: [PATCH 5/6] clippy --- tools/git-scraper/src/fetch.rs | 4 +++- tools/git-scraper/src/init.rs | 1 - tools/git-scraper/src/write.rs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/git-scraper/src/fetch.rs b/tools/git-scraper/src/fetch.rs index 4c2ac7b2b2..914ce51785 100644 --- a/tools/git-scraper/src/fetch.rs +++ b/tools/git-scraper/src/fetch.rs @@ -1,6 +1,8 @@ use reqwest::blocking::Client; use serde_json::Value; +type InteractorContent = (Vec<(String, String)>, Option); + pub(crate) fn fetch_directory_listing(client: &Client, url: &str) -> reqwest::Result { println!("Fetching directory listing from: {}", url); let response = client @@ -73,7 +75,7 @@ pub(crate) fn fetch_directory_contents( pub(crate) fn fetch_interactor_contents( client: &Client, folder_url: &str, -) -> Option<(Vec<(String, String)>, Option)> { +) -> Option { println!("Fetching interactor contents from {}", folder_url); let folder_response: Value = client.get(folder_url).send().ok()?.json().ok()?; diff --git a/tools/git-scraper/src/init.rs b/tools/git-scraper/src/init.rs index a1f7dac9ef..d97584ef0b 100644 --- a/tools/git-scraper/src/init.rs +++ b/tools/git-scraper/src/init.rs @@ -3,7 +3,6 @@ use serde::Deserialize; use std::fs; use std::fs::File; use std::io::{self, BufWriter}; -use toml; #[derive(Deserialize)] struct Config { diff --git a/tools/git-scraper/src/write.rs b/tools/git-scraper/src/write.rs index 75ea6f2fa1..3845717de5 100644 --- a/tools/git-scraper/src/write.rs +++ b/tools/git-scraper/src/write.rs @@ -211,7 +211,7 @@ pub(crate) fn cleanup_local_paths(file_path: &str) -> io::Result<()> { Ok(()) } -fn has_local_path_next<'a>(lines: &[&'a str], current: &str) -> bool { +fn has_local_path_next(lines: &[&str], current: &str) -> bool { lines .iter() .skip_while(|&&x| x != current) From 4fa78d855f1d9f2dac5c1d4f6cdc6098ea1fc488 Mon Sep 17 00:00:00 2001 From: Mihai Calin Luca Date: Fri, 17 Jan 2025 17:24:20 +0100 Subject: [PATCH 6/6] fetch all sc files, more instructions --- tools/git-scraper/contracts_dump.txt | 539 +++++++++++++++++++++++++++ tools/git-scraper/src/fetch.rs | 52 +++ tools/git-scraper/src/scraper.rs | 5 +- tools/git-scraper/src/write.rs | 58 ++- 4 files changed, 652 insertions(+), 2 deletions(-) diff --git a/tools/git-scraper/contracts_dump.txt b/tools/git-scraper/contracts_dump.txt index 3aecc4c4f7..9485a28924 100644 --- a/tools/git-scraper/contracts_dump.txt +++ b/tools/git-scraper/contracts_dump.txt @@ -8,6 +8,31 @@ INSTRUCTIONS FOR USING THIS FILE - CARGO.TOML: Dependencies and contract configuration - INTERACTOR FOLDER: If available, contains interactor files (used for deployment and interaction on the blockchain) 3. Before the contract code dump you will find a step by step description of how to create, build and deploy smart contracts on MultiversX +4. All contracts contain a multiversx.json file with the following content: +{ + "language": "rust" +} +This file is mandatory for sc-meta to recognize our contract crate. You should generate it for all contracts. +5. If the contract contains an interactor/ folder, the file structure should change in order to compile. The contract is by default #[no_std], +while the interactor uses std, so they cannot be compiled at once. We need an external workspace containing both of them in order to compile. +For example, if we have adder/ contract which contains adder/interactor, we should create an outer workspace containing the adder, as such: + +Cargo.toml + +[workspace] +resolver = "2" +members = ["adder", "adder/meta", "adder/interactor] + +Folder structure: + +outer-folder/ +|---> adder/ + |---> src/ + |---> Cargo.toml + |---> interactor/ + |---> src/ + |---> Cargo.toml +|---> Cargo.toml INSTRUCTIONS FOR CREATING, BUILDING AND DEPLOYING SMART CONTRACTS ON MULTIVERSX ============================== @@ -268,7 +293,31 @@ version = "0.55.0" [dev-dependencies.multiversx-sc-scenario] version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "adder-meta" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies.adder] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: +SRC FOLDER: FILE_NAME: basic_interactor.rs mod basic_interactor_cli; mod basic_interactor_config; @@ -788,6 +837,30 @@ version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "bonding-curve-contract-meta" +version = "0.0.0" +authors = ["Alin Cruceat "] +edition = "2021" +publish = false + +[dependencies.bonding-curve-contract] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: check-pause @@ -840,6 +913,30 @@ version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "check-pause-meta" +version = "0.0.0" +edition = "2021" +publish = false +authors = ["Alin Cruceat "] + +[dependencies.check-pause] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: crowdfunding-esdt @@ -1161,6 +1258,30 @@ num-traits = "0.2" hex = "0.4" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "crowdfunding-esdt-meta" +version = "0.0.0" +authors = ["Andrei Marinica "] +edition = "2021" +publish = false + +[dependencies.crowdfunding-esdt] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: crypto-bubbles @@ -1313,6 +1434,30 @@ version = "0.55.0" version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "crypto-bubbles-meta" +version = "0.0.0" +authors = ["Andrei Marinica "] +edition = "2021" +publish = false + +[dependencies.crypto-bubbles] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: crypto-kitties @@ -1323,6 +1468,8 @@ None SRC FOLDER: No src folder found +META FOLDER: + INTERACTOR FOLDER: //////////////////////// NAME: crypto-zombies @@ -2388,6 +2535,29 @@ version = "0.55.0" version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "crypto-zombies-meta" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies.crypto-zombies] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: digital-cash @@ -3223,6 +3393,29 @@ version = "0.55.0" version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "digital-cash-meta" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies.digital-cash] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: empty @@ -3269,6 +3462,29 @@ version = "0.55.0" num-bigint = "0.4" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "empty-meta" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies.empty] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: esdt-transfer-with-fee @@ -3460,6 +3676,29 @@ version = "0.55.0" version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "esdt-transfer-with-fee-meta" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies.esdt-transfer-with-fee] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: factorial @@ -3519,6 +3758,30 @@ version = "0.55.0" version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "factorial-meta" +version = "0.0.0" +authors = ["Andrei Marinica "] +edition = "2021" +publish = false + +[dependencies.factorial] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: fractional-nfts @@ -3792,6 +4055,30 @@ version = "0.55.0" version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "fractional-nfts-meta" +version = "0.0.0" +authors = ["Claudiu-Marcel Bruda "] +edition = "2021" +publish = false + +[dependencies.fractional-nfts] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: lottery-esdt @@ -4243,6 +4530,30 @@ version = "0.55.0" version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "lottery-esdt-meta" +version = "0.0.0" +authors = ["Andrei Marinica "] +edition = "2021" +publish = false + +[dependencies.lottery-esdt] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: multisig @@ -5952,6 +6263,30 @@ num-traits = "0.2" hex = "0.4" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "multisig-meta" +version = "0.0.0" +authors = ["Andrei Marinica "] +edition = "2021" +publish = false + +[dependencies.multisig] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: nft-minter @@ -6317,6 +6652,30 @@ version = "0.55.0" version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "nft-minter-meta" +version = "0.0.0" +authors = ["Dorin Iancu "] +edition = "2021" +publish = false + +[dependencies.nft-minter] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: nft-storage-prepay @@ -6451,6 +6810,32 @@ version = "0.55.0" version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "nft-storage-prepay-meta" +version = "0.0.0" +authors = ["Dorin Iancu "] +edition = "2021" +publish = false + +[dev-dependencies] + +[dependencies.nft-storage-prepay] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: nft-subscription @@ -6589,6 +6974,30 @@ version = "0.55.0" version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "nft-subscription-meta" +version = "0.0.0" +authors = ["Thouny "] +edition = "2021" +publish = false + +[dependencies.nft-subscription] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: order-book @@ -6599,6 +7008,8 @@ None SRC FOLDER: No src folder found +META FOLDER: + INTERACTOR FOLDER: //////////////////////// NAME: ping-pong-egld @@ -7191,7 +7602,32 @@ version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "ping-pong-egld-meta" +version = "0.0.0" +authors = ["Bruda Claudiu-Marcel "] +edition = "2021" +publish = false + +[dependencies.ping-pong-egld] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: +SRC FOLDER: FILE_NAME: interact.rs mod interact_cli; mod interact_config; @@ -7955,6 +8391,32 @@ path = "../check-pause" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "proxy-pause-meta" +version = "0.0.0" +edition = "2021" +publish = false +authors = ["you"] + +[dev-dependencies] + +[dependencies.proxy-pause] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: rewards-distribution @@ -8888,6 +9350,32 @@ version = "0.55.0" version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "rewards-distribution-meta" +version = "0.0.0" +edition = "2021" +publish = false +authors = ["Claudiu-Marcel Bruda "] + +[dev-dependencies] + +[dependencies.rewards-distribution] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: seed-nft-minter @@ -9337,6 +9825,32 @@ version = "0.55.0" version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "seed-nft-minter-meta" +version = "0.0.0" +edition = "2021" +publish = false +authors = ["Claudiu-Marcel Bruda "] + +[dev-dependencies] + +[dependencies.seed-nft-minter] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// NAME: token-release @@ -9764,5 +10278,30 @@ version = "0.55.0" +META FOLDER: +SRC FOLDER: +FILE_NAME: main.rs +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} + + +META CARGO.TOML: +[package] +name = "token-release-meta" +version = "0.0.0" +edition = "2021" +publish = false + +[dev-dependencies] + +[dependencies.token-release] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.55.0" +default-features = false + + INTERACTOR FOLDER: //////////////////////// \ No newline at end of file diff --git a/tools/git-scraper/src/fetch.rs b/tools/git-scraper/src/fetch.rs index 914ce51785..d54f52fa98 100644 --- a/tools/git-scraper/src/fetch.rs +++ b/tools/git-scraper/src/fetch.rs @@ -148,3 +148,55 @@ fn fetch_files_from_directory(client: &Client, url: &str) -> Option Option { + println!("Fetching meta contents from {}", folder_url); + + let folder_response: Value = client.get(folder_url).send().ok()?.json().ok()?; + + if let Some(entries) = folder_response.as_array() { + let mut src_contents = None; + let mut cargo_contents = None; + + for entry in entries { + if let Some(name) = entry["name"].as_str() { + if name == "meta" { + if let Some(url) = entry["url"].as_str() { + println!("Found meta directory"); + let interactor_response: Value = + client.get(url).send().ok()?.json().ok()?; + + if let Some(interactor_entries) = interactor_response.as_array() { + for interactor_entry in interactor_entries { + match interactor_entry["name"].as_str() { + Some("src") => { + if let Some(src_url) = interactor_entry["url"].as_str() { + src_contents = + fetch_files_from_directory(client, src_url); + } + }, + Some("Cargo.toml") => { + if let Some(download_url) = + interactor_entry["download_url"].as_str() + { + if let Ok(content) = + client.get(download_url).send().unwrap().text() + { + cargo_contents = Some(content); + } + } + }, + _ => {}, + } + } + } + } + } + } + } + + return Some((src_contents.unwrap_or_default(), cargo_contents)); + } + + None +} diff --git a/tools/git-scraper/src/scraper.rs b/tools/git-scraper/src/scraper.rs index 72cdca2ece..bc6a1eff25 100644 --- a/tools/git-scraper/src/scraper.rs +++ b/tools/git-scraper/src/scraper.rs @@ -7,7 +7,7 @@ use std::io::{self, BufWriter, Write}; use std::{thread, time::Duration}; use write::{ cleanup_local_paths, write_cargo_toml, write_instructions, write_interactor_files, - write_readme, write_src_folder, + write_meta_folder, write_readme, write_src_folder, }; mod fetch; @@ -61,6 +61,9 @@ fn process_entry(client: &Client, entry: &Value, writer: &mut BufWriter) - write_cargo_toml(client, folder_url, writer, folder_name)?; thread::sleep(Duration::from_millis(100)); + write_meta_folder(client, folder_url, writer, folder_name)?; + thread::sleep(Duration::from_millis(100)); + write_interactor_files(client, folder_url, writer, folder_name)?; writer.flush()?; diff --git a/tools/git-scraper/src/write.rs b/tools/git-scraper/src/write.rs index 3845717de5..18670c92d8 100644 --- a/tools/git-scraper/src/write.rs +++ b/tools/git-scraper/src/write.rs @@ -1,4 +1,6 @@ -use crate::fetch::{fetch_directory_contents, fetch_file_content, fetch_interactor_contents}; +use crate::fetch::{ + fetch_directory_contents, fetch_file_content, fetch_interactor_contents, fetch_meta_contents, +}; use reqwest::blocking::Client; use std::fs; use std::fs::File; @@ -15,6 +17,31 @@ pub(crate) fn write_instructions(writer: &mut BufWriter) -> io::Result<()> - CARGO.TOML: Dependencies and contract configuration - INTERACTOR FOLDER: If available, contains interactor files (used for deployment and interaction on the blockchain) 3. Before the contract code dump you will find a step by step description of how to create, build and deploy smart contracts on MultiversX +4. All contracts contain a multiversx.json file with the following content: +{ + "language": "rust" +} +This file is mandatory for sc-meta to recognize our contract crate. You should generate it for all contracts. +5. If the contract contains an interactor/ folder, the file structure should change in order to compile. The contract is by default #[no_std], +while the interactor uses std, so they cannot be compiled at once. We need an external workspace containing both of them in order to compile. +For example, if we have adder/ contract which contains adder/interactor, we should create an outer workspace containing the adder, as such: + +Cargo.toml + +[workspace] +resolver = "2" +members = ["adder", "adder/meta", "adder/interactor] + +Folder structure: + +outer-folder/ +|---> adder/ + |---> src/ + |---> Cargo.toml + |---> interactor/ + |---> src/ + |---> Cargo.toml +|---> Cargo.toml INSTRUCTIONS FOR CREATING, BUILDING AND DEPLOYING SMART CONTRACTS ON MULTIVERSX ============================== @@ -167,6 +194,7 @@ pub(crate) fn write_interactor_files( writeln!(writer, "\nINTERACTOR FOLDER:")?; if !src_files.is_empty() { + writeln!(writer, "SRC FOLDER:")?; for (file_name, file_content) in src_files { writeln!(writer, "FILE_NAME: {}", file_name)?; writeln!(writer, "{}", file_content)?; @@ -184,6 +212,34 @@ pub(crate) fn write_interactor_files( Ok(()) } +pub(crate) fn write_meta_folder( + client: &Client, + folder_url: &str, + writer: &mut BufWriter, + folder_name: &str, +) -> io::Result<()> { + if let Some((src_files, cargo_content)) = fetch_meta_contents(client, folder_url) { + writeln!(writer, "\nMETA FOLDER:")?; + + if !src_files.is_empty() { + writeln!(writer, "SRC FOLDER:")?; + for (file_name, file_content) in src_files { + writeln!(writer, "FILE_NAME: {}", file_name)?; + writeln!(writer, "{}", file_content)?; + } + } + + if let Some(cargo_content) = cargo_content { + writeln!(writer, "\nMETA CARGO.TOML:")?; + writeln!(writer, "{}", cargo_content)?; + } + } else { + println!("No meta folder found for {}", folder_name); + } + writer.flush()?; + Ok(()) +} + pub(crate) fn cleanup_local_paths(file_path: &str) -> io::Result<()> { let content = fs::read_to_string(file_path)?; let lines: Vec<&str> = content.lines().collect();