From a5a24cfe5a84861d240ed56eaeb73437d9221a4a Mon Sep 17 00:00:00 2001 From: Marco Date: Mon, 18 Mar 2024 18:51:36 +0100 Subject: [PATCH] Add airdrop (#63) * feat(airdrop): setup package * feat(contract): add airdrop role * feat(contract): add airdrop action * feat(contract): add use airdrop * chore(version): bump contract to 2.4.0 --- Cargo.lock | 19 +++++- Cargo.toml | 1 + airdrop/Cargo.toml | 30 +++++++++ airdrop/src/actions.rs | 19 ++++++ airdrop/src/lib.rs | 5 ++ airdrop/src/state.rs | 34 ++++++++++ airdrop/src/tests.rs | 49 ++++++++++++++ contract/Cargo.toml | 3 +- contract/src/contract.rs | 89 +++++++++++++++++++------ contract/src/state.rs | 4 ++ contract/tests/cucumber.rs | 31 ++++++++- contract/tests/features/airdrop.feature | 12 ++++ 12 files changed, 271 insertions(+), 25 deletions(-) create mode 100644 airdrop/Cargo.toml create mode 100644 airdrop/src/actions.rs create mode 100644 airdrop/src/lib.rs create mode 100644 airdrop/src/state.rs create mode 100644 airdrop/src/tests.rs create mode 100644 contract/tests/features/airdrop.feature diff --git a/Cargo.lock b/Cargo.lock index 9396e8a..5d219d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "airdrop" +version = "1.0.0" +dependencies = [ + "create_type_spec_derive", + "pbc_contract_codegen", + "pbc_contract_common", + "pbc_lib", + "pbc_traits", + "read_write_rpc_derive", + "read_write_state_derive", + "rpc-msg-derive", + "serde_json", + "utils", +] + [[package]] name = "anes" version = "0.1.6" @@ -1029,9 +1045,10 @@ checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "meta-names-contract" -version = "2.3.1" +version = "2.4.0" dependencies = [ "access-control", + "airdrop", "contract-version-base", "create_type_spec_derive", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 642ac15..328e5e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ + "airdrop", "contract", "contract-proxy", "contract-version-base", diff --git a/airdrop/Cargo.toml b/airdrop/Cargo.toml new file mode 100644 index 0000000..7cef78e --- /dev/null +++ b/airdrop/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "airdrop" +version = "1.0.0" +authors = ["Yeboster"] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +pbc_contract_common = { workspace = true } +pbc_contract_codegen = { workspace = true } +pbc_traits = { workspace = true } +pbc_lib = { workspace = true } +read_write_rpc_derive = { workspace = true } +read_write_state_derive = { workspace = true } +create_type_spec_derive = { workspace = true } + +utils = { path = "../utils" } +rpc-msg-derive = { path = "../rpc-msg-derive" } + +serde_json = { workspace = true } + +[features] +abi = [ + "pbc_contract_common/abi", + "pbc_contract_codegen/abi", + "pbc_traits/abi", + "create_type_spec_derive/abi", +] diff --git a/airdrop/src/actions.rs b/airdrop/src/actions.rs new file mode 100644 index 0000000..9e04343 --- /dev/null +++ b/airdrop/src/actions.rs @@ -0,0 +1,19 @@ +use pbc_contract_common::{address::Address, avl_tree_map::AvlTreeMap}; + +use crate::state::AirdropState; + +pub fn execute_init() -> AirdropState { + AirdropState { + inventory: AvlTreeMap::new(), + } +} + +pub fn execute_airdrop(state: &mut AirdropState, address: &Address) { + assert!(state.has_airdrop(address), "No airdrop for the address"); + + state._use_airdrop(address); +} + +pub fn execute_add_airdrop(state: &mut AirdropState, address: &Address) { + state._add_airdrop(address); +} diff --git a/airdrop/src/lib.rs b/airdrop/src/lib.rs new file mode 100644 index 0000000..3496451 --- /dev/null +++ b/airdrop/src/lib.rs @@ -0,0 +1,5 @@ +pub mod actions; +pub mod state; + +#[cfg(test)] +mod tests; diff --git a/airdrop/src/state.rs b/airdrop/src/state.rs new file mode 100644 index 0000000..d4544af --- /dev/null +++ b/airdrop/src/state.rs @@ -0,0 +1,34 @@ +use create_type_spec_derive::CreateTypeSpec; +use pbc_contract_common::{address::Address, avl_tree_map::AvlTreeMap}; +use read_write_state_derive::ReadWriteState; + +#[repr(C)] +#[derive(ReadWriteState, CreateTypeSpec, Default, Debug)] +pub struct AirdropState { + pub inventory: AvlTreeMap, +} + +impl AirdropState { + /// Check if the address has an airdrop + pub fn has_airdrop(&self, address: &Address) -> bool { + self.inventory.contains_key(address) + } + + /// Use airdrop from the address + pub fn _use_airdrop(&mut self, address: &Address) { + if let Some(airdrop) = self.inventory.get(address) { + let remaining = airdrop - 1; + if remaining == 0 { + self.inventory.remove(address); + } else { + self.inventory.insert(*address, remaining); + } + } + } + + /// Add airdrop to the address + pub fn _add_airdrop(&mut self, address: &Address) { + let airdrop = self.inventory.get(address).unwrap_or(0); + self.inventory.insert(*address, airdrop + 1); + } +} diff --git a/airdrop/src/tests.rs b/airdrop/src/tests.rs new file mode 100644 index 0000000..0fcdb8a --- /dev/null +++ b/airdrop/src/tests.rs @@ -0,0 +1,49 @@ +// Setup tests + +use utils::tests::mock_address; + +use crate::actions::{execute_add_airdrop, execute_airdrop, execute_init}; + +#[test] +fn proper_has_airdrop() { + let address = mock_address(0); + let mut state = execute_init(); + + assert_eq!(state.has_airdrop(&address), false); + + execute_add_airdrop(&mut state, &address); + + assert_eq!(state.has_airdrop(&address), true); +} + +#[test] +fn proper_execute_airdrop() { + let address = mock_address(0); + let mut state = execute_init(); + + execute_add_airdrop(&mut state, &address); + execute_airdrop(&mut state, &address); + + assert_eq!(state.has_airdrop(&address), false); +} + +#[test] +fn proper_execute_add_airdrop() { + let address = mock_address(0); + let mut state = execute_init(); + + execute_add_airdrop(&mut state, &address); + execute_add_airdrop(&mut state, &address); + + assert_eq!(state.has_airdrop(&address), true); + assert_eq!(state.inventory.get(&address).unwrap(), 2); +} + +#[test] +#[should_panic(expected = "No airdrop for the address")] +fn proper_execute_airdrop_no_airdrop() { + let address = mock_address(0); + let mut state = execute_init(); + + execute_airdrop(&mut state, &address); +} diff --git a/contract/Cargo.toml b/contract/Cargo.toml index f85cb26..8bfc996 100644 --- a/contract/Cargo.toml +++ b/contract/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "meta-names-contract" -version = "2.3.1" +version = "2.4.0" authors = ["Yeboster"] edition = "2021" @@ -17,6 +17,7 @@ read_write_state_derive = { workspace = true } create_type_spec_derive = { workspace = true } access-control = { path = "../access-control" } +airdrop = { path = "../airdrop" } contract-version-base = { path = "../contract-version-base" } partisia-name-system = { path = "../partisia-name-system" } nft = { path = "../nft" } diff --git a/contract/src/contract.rs b/contract/src/contract.rs index a0b4905..b6c4c7a 100644 --- a/contract/src/contract.rs +++ b/contract/src/contract.rs @@ -17,6 +17,7 @@ use pbc_contract_common::{ use nft::{actions as nft_actions, msg as nft_msg}; use access_control::{actions as ac_actions, msg as ac_msg}; +use airdrop::actions::{self as airdrop_actions, execute_airdrop}; use partisia_name_system::{actions as pns_actions, msg as pns_msg, state::RecordClass}; use utils::events::assert_callback_success; @@ -58,11 +59,13 @@ pub fn initialize(ctx: ContractContext, msg: InitMsg) -> (ContractState, Vec, +) -> (ContractState, Vec) { + assert_has_role(&state, UserRole::Airdrop {}, &ctx.sender); + + for address in addresses { + airdrop_actions::execute_add_airdrop(&mut state.airdrop, &address); + } + + (state, vec![]) +} + #[callback(shortname = 0x30)] pub fn on_mint_callback( ctx: ContractContext, @@ -529,33 +547,60 @@ fn mint_domain( ); } - let payment_info = assert_and_get_payment_info(config, *payment_coin_id); - let subscription_years = subscription_years.unwrap_or(1); - let total_fees = payment_info.fees.get(domain) * subscription_years as u128; - let payout_transfer_events = action_build_mint_callback( - &PaymentIntent { - id: *payment_coin_id, - receiver: payment_info.receiver.unwrap(), - token: payment_info.token.unwrap(), - total_fees, - }, - &MintMsg { - domain: domain.to_string(), - to: *to, - payment_coin_id: *payment_coin_id, - token_uri: token_uri.clone(), - parent_id: parent_id.clone(), - subscription_years: Some(subscription_years), - }, - 0x30, - ); + let has_airdrop = mut_state.airdrop.has_airdrop(&ctx.sender); + if has_airdrop { + execute_airdrop(&mut mut_state.airdrop, &ctx.sender); + + let (new_state, mint_events) = action_mint( + ctx, + mut_state, + domain, + to, + token_uri, + parent_id, + subscription_years, + ); + + mut_state = new_state; + + events.extend(mint_events); + } else { + let payment_info = assert_and_get_payment_info(config, *payment_coin_id); + let subscription_years = subscription_years.unwrap_or(1); + let total_fees = payment_info.fees.get(domain) * subscription_years as u128; + let payout_transfer_events = action_build_mint_callback( + &PaymentIntent { + id: *payment_coin_id, + receiver: payment_info.receiver.unwrap(), + token: payment_info.token.unwrap(), + total_fees, + }, + &MintMsg { + domain: domain.to_string(), + to: *to, + payment_coin_id: *payment_coin_id, + token_uri: token_uri.clone(), + parent_id: parent_id.clone(), + subscription_years: Some(subscription_years), + }, + 0x30, + ); - events.extend(payout_transfer_events); + events.extend(payout_transfer_events); + } } (mut_state, events) } +fn assert_has_role(state: &ContractState, role: UserRole, account: &Address) { + assert!( + state.access_control.has_role(role as u8, account), + "{}", + ContractError::Unauthorized + ); +} + fn assert_contract_enabled(state: &ContractState) { assert!( state.config.contract_enabled, diff --git a/contract/src/state.rs b/contract/src/state.rs index a4643c2..10aaa4f 100644 --- a/contract/src/state.rs +++ b/contract/src/state.rs @@ -1,4 +1,5 @@ use access_control::state::AccessControlState; +use airdrop::state::AirdropState; use contract_version_base::state::ContractVersionBase; use create_type_spec_derive::CreateTypeSpec; use nft::state::NFTContractState; @@ -14,6 +15,7 @@ use crate::contract::__PBC_IS_ZK_CONTRACT; #[derive(Default, Debug)] pub struct ContractState { pub access_control: AccessControlState, + pub airdrop: AirdropState, pub config: ContractConfig, pub nft: NFTContractState, pub pns: PartisiaNameSystemState, @@ -38,6 +40,8 @@ pub enum UserRole { Admin {}, #[discriminant(1)] Whitelist {}, + #[discriminant(2)] + Airdrop {}, } #[repr(C)] diff --git a/contract/tests/cucumber.rs b/contract/tests/cucumber.rs index 45aa54f..2513dd4 100644 --- a/contract/tests/cucumber.rs +++ b/contract/tests/cucumber.rs @@ -3,7 +3,7 @@ use std::{mem::take, panic::catch_unwind}; use cucumber::{given, then, when, World}; use meta_names_contract::{ contract::{ - approve_domain, initialize, mint, mint_batch, on_mint_callback, + add_airdrop, approve_domain, initialize, mint, mint_batch, on_mint_callback, on_renew_subscription_callback, renew_subscription, transfer_domain, update_config, update_user_role, }, @@ -32,6 +32,7 @@ pub struct ContractWorld { fn get_user_role(role: String) -> UserRole { match role.as_str() { "admin" => UserRole::Admin {}, + "airdrop" => UserRole::Airdrop {}, "whitelist" => UserRole::Whitelist {}, _ => panic!("Unknown role"), } @@ -376,6 +377,23 @@ fn renew_domain(world: &mut ContractWorld, user: String, domain_name: String, ye } } +#[given(expr = "{word} airdropped to '{word}'")] +#[when(expr = "{word} add airdrop to '{word}'")] +fn airdrop(world: &mut ContractWorld, user: String, to: String) { + let res = catch_unwind(std::panic::AssertUnwindSafe(|| { + let state = take(&mut world.state); + add_airdrop( + mock_contract_context(get_address_for_user(user.clone())), + state, + vec![mock_address(get_address_for_user(to))], + ) + })); + + if let Ok((new_state, _)) = res { + world.state = new_state; + } +} + #[when(expr = "{word} mints '{word}' domain with '{word}' domain as the parent")] #[when(regex = r"(\w+) mints '(.+)' domain without (a parent)")] fn mint_domain_with_parent( @@ -501,6 +519,17 @@ fn domain_expires_in(world: &mut ContractWorld, domain: String, action: String, } } +#[then(regex = r"(\w+) (has|has not) the airdrop")] +fn has_airdrop(world: &mut ContractWorld, user: String, action: String) { + let has_airdrop = world + .state + .airdrop + .has_airdrop(&mock_address(get_address_for_user(user))); + + let has = action == "has"; + assert_eq!(has_airdrop, has); +} + // This runs before everything else, so you can setup things here. fn main() { // You may choose any executor you like (`tokio`, `async-std`, etc.). diff --git a/contract/tests/features/airdrop.feature b/contract/tests/features/airdrop.feature new file mode 100644 index 0000000..5bf5ef7 --- /dev/null +++ b/contract/tests/features/airdrop.feature @@ -0,0 +1,12 @@ +Feature: Airdrop feature + + Scenario: An user with airdrop role can add airdrop + Given a meta names contract + And Alice user with the airdrop role + When Alice add airdrop to 'Bob' + Then Bob has the airdrop + + Scenario: An user without airdrop role cannot add airdrop + Given a meta names contract + When Alice add airdrop to 'Bob' + Then Bob has not the airdrop