From ea483f1a4ad8f0a523895e32cd2e3ff56d301d5b Mon Sep 17 00:00:00 2001 From: Dimitrije Dragasevic Date: Wed, 11 Dec 2024 14:53:18 +0100 Subject: [PATCH 1/9] Added Discarded state to cf contract (#715) --- CHANGELOG.md | 2 + Cargo.lock | 2 +- .../andromeda-crowdfund/Cargo.toml | 2 +- .../andromeda-crowdfund/src/contract.rs | 102 +++++----- .../andromeda-crowdfund/src/testing/tests.rs | 178 ++++++++++++------ .../src/crowdfund.rs | 3 + packages/std/src/error.rs | 3 +- tests-integration/tests/crowdfund_app.rs | 11 +- 8 files changed, 191 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26275e344..5b5b5c1d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added +- Crowdfund, added additional state [(#715)](https://github.com/andromedaprotocol/andromeda-core/pull/715) - Added optional config for Send in Splitter contracts [(#686)](https://github.com/andromedaprotocol/andromeda-core/pull/686) - Added CW20 suppport in Splitter contracts [(#703)](https://github.com/andromedaprotocol/andromeda-core/pull/703) @@ -70,6 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Include ADOBase Version in Schema [(#574)](https://github.com/andromedaprotocol/andromeda-core/pull/574) - Added multi-hop support for IBC [(#604)](https://github.com/andromedaprotocol/andromeda-core/pull/604) + ### Changed - Merkle Root: stage expiration now uses `Milliseconds`[(#417)](https://github.com/andromedaprotocol/andromeda-core/pull/417) diff --git a/Cargo.lock b/Cargo.lock index 371f6e488..77e4e6d68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,7 +248,7 @@ dependencies = [ [[package]] name = "andromeda-crowdfund" -version = "2.1.4-beta" +version = "2.1.5-beta" dependencies = [ "andromeda-app", "andromeda-non-fungible-tokens", diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/Cargo.toml b/contracts/non-fungible-tokens/andromeda-crowdfund/Cargo.toml index e33bac69a..ecf9464d7 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/Cargo.toml +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "andromeda-crowdfund" -version = "2.1.4-beta" +version = "2.1.5-beta" edition = "2021" rust-version = "1.75.0" diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs index 5db4bd69e..dbad9f6c0 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs @@ -141,8 +141,8 @@ pub fn handle_execute(mut ctx: ExecuteContext, msg: ExecuteMsg) -> Result execute_start_campaign(ctx, start_time, end_time, presale), ExecuteMsg::PurchaseTiers { orders } => execute_purchase_tiers(ctx, orders), ExecuteMsg::Receive(msg) => handle_receive_cw20(ctx, msg), - ExecuteMsg::EndCampaign {} => execute_end_campaign(ctx, false), - ExecuteMsg::DiscardCampaign {} => execute_end_campaign(ctx, true), + ExecuteMsg::EndCampaign {} => execute_end_campaign(ctx), + ExecuteMsg::DiscardCampaign {} => execute_discard_campaign(ctx), ExecuteMsg::Claim {} => execute_claim(ctx), _ => ADOContract::default().execute(ctx, msg), }?; @@ -370,9 +370,42 @@ fn handle_receive_cw20( } } +fn execute_discard_campaign(mut ctx: ExecuteContext) -> Result { + nonpayable(&ctx.info)?; + + let ExecuteContext { + ref mut deps, + ref info, + .. + } = ctx; + + // Only owner can discard the campaign + let contract = ADOContract::default(); + ensure!( + contract.is_contract_owner(deps.storage, info.sender.as_str())?, + ContractError::Unauthorized {} + ); + + let curr_stage = get_current_stage(deps.storage); + // Ensure that the campaign is in ONGOING, or READY stage + ensure!( + curr_stage == CampaignStage::ONGOING || curr_stage == CampaignStage::READY, + ContractError::InvalidCampaignOperation { + operation: "discard_campaign".to_string(), + stage: curr_stage.to_string() + } + ); + + // Set to DISCARDED state + set_current_stage(deps.storage, CampaignStage::DISCARDED)?; + + Ok(Response::new() + .add_attribute("action", "discard_campaign") + .add_attribute("result", CampaignStage::DISCARDED.to_string())) +} + fn execute_end_campaign( - mut ctx: ExecuteContext, - is_discard: bool, + mut ctx: ExecuteContext ) -> Result { nonpayable(&ctx.info)?; @@ -390,66 +423,46 @@ fn execute_end_campaign( ContractError::Unauthorized {} ); - // Campaign is finished already successfully - // NOTE: ending failed campaign has no effect and is ignored let curr_stage = get_current_stage(deps.storage); - let action = if is_discard { - "discard_campaign" - } else { - "end_campaign" - }; - ensure!( - curr_stage == CampaignStage::ONGOING - || (is_discard && curr_stage != CampaignStage::SUCCESS), + curr_stage == CampaignStage::ONGOING, ContractError::InvalidCampaignOperation { - operation: action.to_string(), + operation: "end_campaign".to_string(), stage: curr_stage.to_string() } ); + let duration = get_duration(deps.storage)?; let current_capital = get_current_capital(deps.storage); let campaign_config = get_config(deps.storage)?; - let duration = get_duration(deps.storage)?; let soft_cap = campaign_config.soft_cap.unwrap_or(Uint128::one()); - let end_time = duration.end_time; - - // Decide the next stage - let next_stage = match ( - is_discard, - current_capital >= soft_cap, - end_time.is_expired(&env.block), - ) { - // discard the campaign as there are some unexpected issues - (true, _, _) => CampaignStage::FAILED, - // Capital hit the target capital and thus campaign is successful - (false, true, _) => CampaignStage::SUCCESS, - // Capital did hit the target capital and is expired, failed - (false, false, true) => CampaignStage::FAILED, - // Capital did not hit the target capital and campaign is not expired - (false, false, false) => { - if current_capital != Uint128::zero() { - // Need to wait until campaign expires - return Err(ContractError::CampaignNotExpired {}); - } - // No capital is gained and thus it can be paused and restart again - CampaignStage::READY + + // Decide the next stage based on capital and expiry + let final_stage = match (duration.end_time.is_expired(&env.block), current_capital >= soft_cap) { + // Success if soft cap is met + (_, true) => CampaignStage::SUCCESS, + // Failed if expired and soft cap not met + (true, false) => CampaignStage::FAILED, + // Error only if not expired and soft cap not met + (false, false) => { + return Err(ContractError::CampaignNotExpired {}); } }; - set_current_stage(deps.storage, next_stage.clone())?; + set_current_stage(deps.storage, final_stage.clone())?; let mut resp = Response::new() - .add_attribute("action", action) - .add_attribute("result", next_stage.to_string()); - if next_stage == CampaignStage::SUCCESS { + .add_attribute("action", "end_campaign") + .add_attribute("result", final_stage.to_string()); + + // If campaign is successful, withdraw funds to recipient + if final_stage == CampaignStage::SUCCESS { let campaign_denom = match campaign_config.denom { Asset::Cw20Token(ref cw20_token) => Asset::Cw20Token(AndrAddr::from_string( cw20_token.get_raw_address(&deps.as_ref())?.to_string(), )), denom => denom, }; - resp = resp.add_submessage(withdraw_to_recipient( ctx, campaign_config.withdrawal_recipient, @@ -457,6 +470,7 @@ fn execute_end_campaign( campaign_denom, )?); } + Ok(resp) } @@ -618,7 +632,7 @@ fn execute_claim(ctx: ExecuteContext) -> Result { let sub_response = match curr_stage { CampaignStage::SUCCESS => handle_successful_claim(deps.branch(), &info.sender)?, - CampaignStage::FAILED => handle_failed_claim(deps.branch(), &info.sender)?, + CampaignStage::FAILED | CampaignStage::DISCARDED => handle_failed_claim(deps.branch(), &info.sender)?, _ => { return Err(ContractError::InvalidCampaignOperation { operation: "Claim".to_string(), diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs index c12c6a956..6b9155a6e 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs @@ -1092,7 +1092,6 @@ mod test { soft_cap: Option, end_time: MillisecondsExpiration, denom: Asset, - is_discard: bool, expected_res: Result, expected_stage: CampaignStage, } @@ -1126,7 +1125,6 @@ mod test { soft_cap: Some(Uint128::new(9000u128)), end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds()), denom: Asset::NativeToken(MOCK_NATIVE_DENOM.to_string()), - is_discard: false, expected_res: Ok(Response::new() .add_attribute("action", "end_campaign") .add_attribute("result", CampaignStage::SUCCESS.to_string()) @@ -1153,7 +1151,6 @@ mod test { soft_cap: Some(Uint128::new(9000u128)), end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds()), denom: Asset::Cw20Token(AndrAddr::from_string(MOCK_CW20_CONTRACT.to_string())), - is_discard: false, expected_res: Ok(Response::new() .add_attribute("action", "end_campaign") .add_attribute("result", CampaignStage::SUCCESS.to_string()) @@ -1190,7 +1187,6 @@ mod test { soft_cap: Some(Uint128::new(11000u128)), end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds()), denom: Asset::Cw20Token(AndrAddr::from_string(MOCK_CW20_CONTRACT.to_string())), - is_discard: false, expected_res: Ok(Response::new() .add_attribute("action", "end_campaign") .add_attribute("result", CampaignStage::FAILED.to_string()) @@ -1209,56 +1205,15 @@ mod test { expected_stage: CampaignStage::FAILED, }, EndCampaignTestCase { - name: "Discard campaign using native token".to_string(), - stage: CampaignStage::ONGOING, - sender: MOCK_DEFAULT_OWNER.to_string(), - current_capital: Uint128::new(10000u128), - soft_cap: Some(Uint128::new(9000u128)), - end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds()), - denom: Asset::NativeToken(MOCK_NATIVE_DENOM.to_string()), - is_discard: true, - expected_res: Ok(Response::new() - .add_attribute("action", "discard_campaign") - .add_attribute("result", CampaignStage::FAILED.to_string()) - .add_submessage(SubMsg::reply_on_error( - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "economics_contract".to_string(), - msg: to_json_binary(&EconomicsExecuteMsg::PayFee { - payee: Addr::unchecked(MOCK_DEFAULT_OWNER.to_string()), - action: "DiscardCampaign".to_string(), - }) - .unwrap(), - funds: vec![], - }), - ReplyId::PayFee.repr(), - ))), - expected_stage: CampaignStage::FAILED, - }, - EndCampaignTestCase { - name: "Pause campaign".to_string(), + name: "Cannot end non-expired campaign".to_string(), // Changed name to better reflect behavior stage: CampaignStage::ONGOING, sender: MOCK_DEFAULT_OWNER.to_string(), current_capital: Uint128::new(0u128), soft_cap: None, end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 1000), denom: Asset::Cw20Token(AndrAddr::from_string(MOCK_CW20_CONTRACT.to_string())), - is_discard: false, - expected_res: Ok(Response::new() - .add_attribute("action", "end_campaign") - .add_attribute("result", CampaignStage::READY.to_string()) - .add_submessage(SubMsg::reply_on_error( - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "economics_contract".to_string(), - msg: to_json_binary(&EconomicsExecuteMsg::PayFee { - payee: Addr::unchecked(MOCK_DEFAULT_OWNER.to_string()), - action: "EndCampaign".to_string(), - }) - .unwrap(), - funds: vec![], - }), - ReplyId::PayFee.repr(), - ))), - expected_stage: CampaignStage::READY, + expected_res: Err(ContractError::CampaignNotExpired {}), + expected_stage: CampaignStage::ONGOING, // Stage won't change on error }, EndCampaignTestCase { name: "End campaign from unauthorized sender".to_string(), @@ -1268,7 +1223,6 @@ mod test { soft_cap: None, end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds()), denom: Asset::NativeToken(MOCK_NATIVE_DENOM.to_string()), - is_discard: false, expected_res: Err(ContractError::Unauthorized {}), expected_stage: CampaignStage::ONGOING, }, @@ -1280,7 +1234,6 @@ mod test { soft_cap: None, end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds()), denom: Asset::NativeToken(MOCK_NATIVE_DENOM.to_string()), - is_discard: false, expected_res: Err(ContractError::InvalidCampaignOperation { operation: "end_campaign".to_string(), stage: CampaignStage::READY.to_string(), @@ -1295,7 +1248,7 @@ mod test { soft_cap: Some(Uint128::new(11000u128)), end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), denom: Asset::NativeToken(MOCK_NATIVE_DENOM.to_string()), - is_discard: false, + expected_res: Err(ContractError::CampaignNotExpired {}), expected_stage: CampaignStage::ONGOING, }, @@ -1317,11 +1270,7 @@ mod test { set_campaign_config(deps.as_mut().storage, &mock_config); set_campaign_duration(deps.as_mut().storage, &duration); - let msg = if test.is_discard { - ExecuteMsg::DiscardCampaign {} - } else { - ExecuteMsg::EndCampaign {} - }; + let msg = ExecuteMsg::EndCampaign {}; let res = execute(deps.as_mut(), env.clone(), info, msg); assert_eq!(res, test.expected_res, "Test case: {}", test.name); @@ -1338,6 +1287,123 @@ mod test { } } + struct DiscardCampaign { + name: String, + stage: CampaignStage, + sender: String, + current_capital: Uint128, + soft_cap: Option, + end_time: MillisecondsExpiration, + denom: Asset, + expected_res: Result, + expected_stage: CampaignStage, + } + + #[test] + fn test_execute_discard_campaign() { + let env: Env = mock_env(); + + let test_cases: Vec = vec![ + DiscardCampaign { + name: "Discard campaign using native token".to_string(), + stage: CampaignStage::READY, + sender: MOCK_DEFAULT_OWNER.to_string(), + current_capital: Uint128::zero(), + soft_cap: Some(Uint128::new(9000u128)), + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds()), + denom: Asset::NativeToken(MOCK_NATIVE_DENOM.to_string()), + expected_res: Ok(Response::new() + .add_attribute("action", "discard_campaign") + .add_attribute("result", "DISCARDED") + .add_submessage(SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "economics_contract".to_string(), + msg: to_json_binary(&EconomicsExecuteMsg::PayFee { + payee: Addr::unchecked(MOCK_DEFAULT_OWNER), + action: "DiscardCampaign".to_string(), + }) + .unwrap(), + funds: vec![], + }), + ReplyId::PayFee.repr(), + ))), + expected_stage: CampaignStage::DISCARDED, + }, + DiscardCampaign { + name: "Cannot discard campaign in SUCCESS state".to_string(), + stage: CampaignStage::SUCCESS, + sender: MOCK_DEFAULT_OWNER.to_string(), + current_capital: Uint128::zero(), + soft_cap: Some(Uint128::new(9000u128)), + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds()), + denom: Asset::NativeToken(MOCK_NATIVE_DENOM.to_string()), + expected_res: Err(ContractError::InvalidCampaignOperation { + operation: "discard_campaign".to_string(), + stage: CampaignStage::SUCCESS.to_string(), + }), + expected_stage: CampaignStage::SUCCESS, + }, + DiscardCampaign { + name: "Cannot discard campaign in FAILED state".to_string(), + stage: CampaignStage::FAILED, + sender: MOCK_DEFAULT_OWNER.to_string(), + current_capital: Uint128::zero(), + soft_cap: Some(Uint128::new(9000u128)), + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds()), + denom: Asset::NativeToken(MOCK_NATIVE_DENOM.to_string()), + expected_res: Err(ContractError::InvalidCampaignOperation { + operation: "discard_campaign".to_string(), + stage: CampaignStage::FAILED.to_string(), + }), + expected_stage: CampaignStage::FAILED, + }, + DiscardCampaign { + name: "Cannot discard campaign with unauthorized sender".to_string(), + stage: CampaignStage::READY, + sender: "unauthorized".to_string(), + current_capital: Uint128::zero(), + soft_cap: Some(Uint128::new(9000u128)), + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds()), + denom: Asset::NativeToken(MOCK_NATIVE_DENOM.to_string()), + expected_res: Err(ContractError::Unauthorized {}), + expected_stage: CampaignStage::READY, + }, + ]; + + for test in test_cases { + let mut deps = mock_dependencies_custom(&[coin(100000, MOCK_NATIVE_DENOM)]); + let mut mock_config = mock_campaign_config(test.denom.clone()); + let _ = init(deps.as_mut(), mock_config.clone(), vec![]); + + let info = mock_info(&test.sender, &[]); + set_campaign_stage(deps.as_mut().storage, &test.stage); + set_current_capital(deps.as_mut().storage, &test.current_capital); + + mock_config.soft_cap = test.soft_cap; + let duration = Duration { + start_time: None, + end_time: test.end_time, + }; + + set_campaign_config(deps.as_mut().storage, &mock_config); + set_campaign_duration(deps.as_mut().storage, &duration); + let msg = ExecuteMsg::DiscardCampaign {}; + + let res = execute(deps.as_mut(), env.clone(), info, msg); + assert_eq!(res, test.expected_res, "Test case: {}", test.name); + if res.is_ok() { + assert_eq!( + CAMPAIGN_STAGE + .load(&deps.storage) + .unwrap_or(CampaignStage::DISCARDED), + test.expected_stage, + "Test case: {}", + test.name + ); + } + } + } + struct ClaimTestCase { name: String, stage: CampaignStage, diff --git a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs index e3783aeab..a85f1bd80 100644 --- a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs +++ b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs @@ -118,6 +118,8 @@ pub enum CampaignStage { SUCCESS, /// Stage when campaign failed to meet the target cap before expiration FAILED, + /// Stage when campaign is discarded + DISCARDED } impl ToString for CampaignStage { @@ -128,6 +130,7 @@ impl ToString for CampaignStage { Self::ONGOING => "ONGOING".to_string(), Self::SUCCESS => "SUCCESS".to_string(), Self::FAILED => "FAILED".to_string(), + Self::DISCARDED => "DISCARDED".to_string(), } } } diff --git a/packages/std/src/error.rs b/packages/std/src/error.rs index 2d652287f..4a2775b2f 100644 --- a/packages/std/src/error.rs +++ b/packages/std/src/error.rs @@ -72,9 +72,10 @@ pub enum ContractError { operation: String, validator: String, }, + #[error("Invalid Campaign Operation: {operation} on {stage}")] InvalidCampaignOperation { operation: String, stage: String }, - + #[error("No Staking Reward")] InvalidClaim {}, diff --git a/tests-integration/tests/crowdfund_app.rs b/tests-integration/tests/crowdfund_app.rs index e9cd015b2..05d697e86 100644 --- a/tests-integration/tests/crowdfund_app.rs +++ b/tests-integration/tests/crowdfund_app.rs @@ -408,19 +408,12 @@ fn test_crowdfund_app_native_discard( buyer_one_original_balance - Uint128::new(10 * 100 + 200 * 10) ); - let _ = crowdfund.execute_end_campaign(owner.clone(), &mut router); - - let summary = crowdfund.query_campaign_summary(&mut router); - - // Campaign could not be ended due to invalid withdrawal recipient msg - assert_eq!(summary.current_stage, CampaignStage::ONGOING.to_string()); - // Discard campaign let _ = crowdfund.execute_discard_campaign(owner.clone(), &mut router); let summary = crowdfund.query_campaign_summary(&mut router); - assert_eq!(summary.current_stage, CampaignStage::FAILED.to_string()); + assert_eq!(summary.current_stage, CampaignStage::DISCARDED.to_string()); - // Refund + // Verify refunds after discard let buyer_one_original_balance = router .wrap() .query_balance(buyer_one.clone(), "uandr") From 282fa0f48dcee4f42e576b59e045e0bbd7bdb9ec Mon Sep 17 00:00:00 2001 From: Mitar Djakovic Date: Wed, 11 Dec 2024 17:00:32 +0200 Subject: [PATCH 2/9] Moved Distance ADO to Andromeda-math (#714) --- Cargo.lock | 2 ++ .../andromeda-distance/.cargo/config | 0 .../andromeda-distance/Cargo.toml | 1 + .../andromeda-distance/examples/schema.rs | 0 .../andromeda-distance/src/contract.rs | 13 ++++++++++++- .../math/andromeda-distance/src/interface.rs | 6 ++++++ contracts/math/andromeda-distance/src/lib.rs | 11 +++++++++++ .../andromeda-distance/src/mock.rs | 2 +- .../andromeda-distance/src/testing/mock.rs | 0 .../andromeda-distance/src/testing/mod.rs | 0 .../andromeda-distance/src/testing/tests.rs | 0 contracts/modules/andromeda-distance/src/lib.rs | 6 ------ .../andromeda-crowdfund/src/contract.rs | 17 ++++++++++------- .../andromeda-crowdfund/src/testing/tests.rs | 6 +++--- .../src/crowdfund.rs | 2 +- packages/deploy/Cargo.toml | 1 + packages/deploy/src/contracts.rs | 2 ++ packages/std/src/error.rs | 4 ++-- 18 files changed, 52 insertions(+), 21 deletions(-) rename contracts/{modules => math}/andromeda-distance/.cargo/config (100%) rename contracts/{modules => math}/andromeda-distance/Cargo.toml (96%) rename contracts/{modules => math}/andromeda-distance/examples/schema.rs (100%) rename contracts/{modules => math}/andromeda-distance/src/contract.rs (92%) create mode 100644 contracts/math/andromeda-distance/src/interface.rs create mode 100644 contracts/math/andromeda-distance/src/lib.rs rename contracts/{modules => math}/andromeda-distance/src/mock.rs (96%) rename contracts/{modules => math}/andromeda-distance/src/testing/mock.rs (100%) rename contracts/{modules => math}/andromeda-distance/src/testing/mod.rs (100%) rename contracts/{modules => math}/andromeda-distance/src/testing/tests.rs (100%) delete mode 100644 contracts/modules/andromeda-distance/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 77e4e6d68..b3e206e39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -400,6 +400,7 @@ dependencies = [ "andromeda-cw20-staking", "andromeda-cw721", "andromeda-date-time", + "andromeda-distance", "andromeda-economics", "andromeda-ibc-registry", "andromeda-kernel", @@ -463,6 +464,7 @@ dependencies = [ "cosmwasm-schema 1.5.8", "cosmwasm-std 1.5.8", "cw-multi-test", + "cw-orch", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", ] diff --git a/contracts/modules/andromeda-distance/.cargo/config b/contracts/math/andromeda-distance/.cargo/config similarity index 100% rename from contracts/modules/andromeda-distance/.cargo/config rename to contracts/math/andromeda-distance/.cargo/config diff --git a/contracts/modules/andromeda-distance/Cargo.toml b/contracts/math/andromeda-distance/Cargo.toml similarity index 96% rename from contracts/modules/andromeda-distance/Cargo.toml rename to contracts/math/andromeda-distance/Cargo.toml index 38a61c528..d0ac3bef4 100644 --- a/contracts/modules/andromeda-distance/Cargo.toml +++ b/contracts/math/andromeda-distance/Cargo.toml @@ -25,6 +25,7 @@ andromeda-std = { workspace = true } andromeda-math = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +cw-orch = { workspace = true } cw-multi-test = { workspace = true, optional = true } andromeda-testing = { workspace = true, optional = true } diff --git a/contracts/modules/andromeda-distance/examples/schema.rs b/contracts/math/andromeda-distance/examples/schema.rs similarity index 100% rename from contracts/modules/andromeda-distance/examples/schema.rs rename to contracts/math/andromeda-distance/examples/schema.rs diff --git a/contracts/modules/andromeda-distance/src/contract.rs b/contracts/math/andromeda-distance/src/contract.rs similarity index 92% rename from contracts/modules/andromeda-distance/src/contract.rs rename to contracts/math/andromeda-distance/src/contract.rs index 911152cf7..b3e34f243 100644 --- a/contracts/modules/andromeda-distance/src/contract.rs +++ b/contracts/math/andromeda-distance/src/contract.rs @@ -1,6 +1,6 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response}; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError}; use andromeda_math::distance::{Coordinate, DistanceType, ExecuteMsg, InstantiateMsg, QueryMsg}; use andromeda_std::{ @@ -165,3 +165,14 @@ fn decimal_validate(decimal: u16) -> Result<(), ContractError> { pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { ADOContract::default().migrate(deps, CONTRACT_NAME, CONTRACT_VERSION) } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result { + if msg.result.is_err() { + return Err(ContractError::Std(StdError::generic_err( + msg.result.unwrap_err(), + ))); + } + + Ok(Response::default()) +} diff --git a/contracts/math/andromeda-distance/src/interface.rs b/contracts/math/andromeda-distance/src/interface.rs new file mode 100644 index 000000000..9900d8cd9 --- /dev/null +++ b/contracts/math/andromeda-distance/src/interface.rs @@ -0,0 +1,6 @@ +use andromeda_math::distance::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::{ado_base::MigrateMsg, contract_interface, deploy::ADOMetadata}; + +pub const CONTRACT_ID: &str = "distance"; + +contract_interface!(DistanceContract, CONTRACT_ID, "andromeda_distance.wasm"); diff --git a/contracts/math/andromeda-distance/src/lib.rs b/contracts/math/andromeda-distance/src/lib.rs new file mode 100644 index 000000000..f600be9cb --- /dev/null +++ b/contracts/math/andromeda-distance/src/lib.rs @@ -0,0 +1,11 @@ +pub mod contract; +#[cfg(test)] +pub mod testing; + +#[cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +pub mod mock; + +#[cfg(not(target_arch = "wasm32"))] +mod interface; +#[cfg(not(target_arch = "wasm32"))] +pub use crate::interface::DistanceContract; diff --git a/contracts/modules/andromeda-distance/src/mock.rs b/contracts/math/andromeda-distance/src/mock.rs similarity index 96% rename from contracts/modules/andromeda-distance/src/mock.rs rename to contracts/math/andromeda-distance/src/mock.rs index bcc76496c..713abafce 100644 --- a/contracts/modules/andromeda-distance/src/mock.rs +++ b/contracts/math/andromeda-distance/src/mock.rs @@ -1,6 +1,6 @@ #![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] use crate::contract::{execute, instantiate, query}; -use andromeda_modules::distance::{Coordinate, InstantiateMsg, QueryMsg}; +use andromeda_math::distance::{Coordinate, InstantiateMsg, QueryMsg}; use andromeda_testing::mock::MockApp; use andromeda_testing::{ mock_ado, diff --git a/contracts/modules/andromeda-distance/src/testing/mock.rs b/contracts/math/andromeda-distance/src/testing/mock.rs similarity index 100% rename from contracts/modules/andromeda-distance/src/testing/mock.rs rename to contracts/math/andromeda-distance/src/testing/mock.rs diff --git a/contracts/modules/andromeda-distance/src/testing/mod.rs b/contracts/math/andromeda-distance/src/testing/mod.rs similarity index 100% rename from contracts/modules/andromeda-distance/src/testing/mod.rs rename to contracts/math/andromeda-distance/src/testing/mod.rs diff --git a/contracts/modules/andromeda-distance/src/testing/tests.rs b/contracts/math/andromeda-distance/src/testing/tests.rs similarity index 100% rename from contracts/modules/andromeda-distance/src/testing/tests.rs rename to contracts/math/andromeda-distance/src/testing/tests.rs diff --git a/contracts/modules/andromeda-distance/src/lib.rs b/contracts/modules/andromeda-distance/src/lib.rs deleted file mode 100644 index b0b35c28c..000000000 --- a/contracts/modules/andromeda-distance/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod contract; -#[cfg(test)] -pub mod testing; - -#[cfg(all(not(target_arch = "wasm32"), feature = "testing"))] -pub mod mock; diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs index dbad9f6c0..81766cdf7 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs @@ -372,7 +372,7 @@ fn handle_receive_cw20( fn execute_discard_campaign(mut ctx: ExecuteContext) -> Result { nonpayable(&ctx.info)?; - + let ExecuteContext { ref mut deps, ref info, @@ -389,7 +389,7 @@ fn execute_discard_campaign(mut ctx: ExecuteContext) -> Result Result Result { +fn execute_end_campaign(mut ctx: ExecuteContext) -> Result { nonpayable(&ctx.info)?; let ExecuteContext { @@ -438,7 +436,10 @@ fn execute_end_campaign( let soft_cap = campaign_config.soft_cap.unwrap_or(Uint128::one()); // Decide the next stage based on capital and expiry - let final_stage = match (duration.end_time.is_expired(&env.block), current_capital >= soft_cap) { + let final_stage = match ( + duration.end_time.is_expired(&env.block), + current_capital >= soft_cap, + ) { // Success if soft cap is met (_, true) => CampaignStage::SUCCESS, // Failed if expired and soft cap not met @@ -632,7 +633,9 @@ fn execute_claim(ctx: ExecuteContext) -> Result { let sub_response = match curr_stage { CampaignStage::SUCCESS => handle_successful_claim(deps.branch(), &info.sender)?, - CampaignStage::FAILED | CampaignStage::DISCARDED => handle_failed_claim(deps.branch(), &info.sender)?, + CampaignStage::FAILED | CampaignStage::DISCARDED => { + handle_failed_claim(deps.branch(), &info.sender)? + } _ => { return Err(ContractError::InvalidCampaignOperation { operation: "Claim".to_string(), diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs index 6b9155a6e..ebc295079 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs @@ -1205,7 +1205,7 @@ mod test { expected_stage: CampaignStage::FAILED, }, EndCampaignTestCase { - name: "Cannot end non-expired campaign".to_string(), // Changed name to better reflect behavior + name: "Cannot end non-expired campaign".to_string(), // Changed name to better reflect behavior stage: CampaignStage::ONGOING, sender: MOCK_DEFAULT_OWNER.to_string(), current_capital: Uint128::new(0u128), @@ -1213,7 +1213,7 @@ mod test { end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 1000), denom: Asset::Cw20Token(AndrAddr::from_string(MOCK_CW20_CONTRACT.to_string())), expected_res: Err(ContractError::CampaignNotExpired {}), - expected_stage: CampaignStage::ONGOING, // Stage won't change on error + expected_stage: CampaignStage::ONGOING, // Stage won't change on error }, EndCampaignTestCase { name: "End campaign from unauthorized sender".to_string(), @@ -1302,7 +1302,7 @@ mod test { #[test] fn test_execute_discard_campaign() { let env: Env = mock_env(); - + let test_cases: Vec = vec![ DiscardCampaign { name: "Discard campaign using native token".to_string(), diff --git a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs index a85f1bd80..baf1886cf 100644 --- a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs +++ b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs @@ -119,7 +119,7 @@ pub enum CampaignStage { /// Stage when campaign failed to meet the target cap before expiration FAILED, /// Stage when campaign is discarded - DISCARDED + DISCARDED, } impl ToString for CampaignStage { diff --git a/packages/deploy/Cargo.toml b/packages/deploy/Cargo.toml index 6c353367d..57168540f 100644 --- a/packages/deploy/Cargo.toml +++ b/packages/deploy/Cargo.toml @@ -80,6 +80,7 @@ andromeda-rates = { path = "../../contracts/modules/andromeda-rates" } # Math Contracts andromeda-counter = { path = "../../contracts/math/andromeda-counter" } andromeda-curve = { path = "../../contracts/math/andromeda-curve" } +andromeda-distance = { path = "../../contracts/math/andromeda-distance" } andromeda-date-time = { path = "../../contracts/math/andromeda-date-time" } andromeda-shunting = { path = "../../contracts/math/andromeda-shunting" } diff --git a/packages/deploy/src/contracts.rs b/packages/deploy/src/contracts.rs index ef87551ad..52ae094af 100644 --- a/packages/deploy/src/contracts.rs +++ b/packages/deploy/src/contracts.rs @@ -11,6 +11,7 @@ use andromeda_cw20::CW20Contract; use andromeda_cw20_exchange::Cw20ExchangeContract; use andromeda_cw20_staking::CW20StakingContract; use andromeda_cw721::CW721Contract; +use andromeda_distance::DistanceContract; use andromeda_economics::EconomicsContract; use andromeda_ibc_registry::IBCRegistryContract; use andromeda_kernel::KernelContract; @@ -82,6 +83,7 @@ pub fn all_contracts() -> Vec { deployable!(AuctionContract), deployable!(CrowdfundContract), deployable!(MarketplaceContract), + deployable!(DistanceContract), ] } diff --git a/packages/std/src/error.rs b/packages/std/src/error.rs index 4a2775b2f..03b01bfd2 100644 --- a/packages/std/src/error.rs +++ b/packages/std/src/error.rs @@ -72,10 +72,10 @@ pub enum ContractError { operation: String, validator: String, }, - + #[error("Invalid Campaign Operation: {operation} on {stage}")] InvalidCampaignOperation { operation: String, stage: String }, - + #[error("No Staking Reward")] InvalidClaim {}, From 7131b62822fed51e27f428dbed5ba0cab4f826d6 Mon Sep 17 00:00:00 2001 From: Joe Monem <66594578+joemonem@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:37:29 +0200 Subject: [PATCH 3/9] ref: Limit Rates Recipient to One (#669) --- CHANGELOG.md | 4 +- Cargo.lock | 2 +- .../andromeda-boolean/src/testing/tests.rs | 10 +- .../andromeda-primitive/src/testing/tests.rs | 18 +- .../src/testing/tests.rs | 10 +- .../andromeda-splitter/src/contract.rs | 2 +- .../finance/andromeda-splitter/src/mock.rs | 1 + .../andromeda-cw20/src/testing/tests.rs | 8 +- .../math/andromeda-point/src/testing/tests.rs | 10 +- .../modules/andromeda-rates/src/contract.rs | 73 +++-- .../andromeda-rates/src/testing/tests.rs | 16 +- .../andromeda-auction/src/testing/tests.rs | 12 +- .../src/testing/tests.rs | 12 +- contracts/os/andromeda-kernel/src/execute.rs | 1 - packages/std/Cargo.toml | 2 +- packages/std/src/ado_base/rates.rs | 110 +++++-- packages/std/src/ado_contract/execute.rs | 51 ++++ packages/std/src/ado_contract/rates.rs | 18 +- packages/std/src/amp/recipient.rs | 8 + packages/std/src/error.rs | 3 + tests-integration/tests/auction_app.rs | 44 ++- tests-integration/tests/cw20_app.rs | 11 +- tests-integration/tests/kernel_orch.rs | 271 +++++++++++++++++- tests-integration/tests/marketplace_app.rs | 8 +- tests-integration/tests/primitive.rs | 4 +- tests-integration/tests/rates_orch.rs | 221 ++++++++++++++ 26 files changed, 782 insertions(+), 148 deletions(-) create mode 100644 tests-integration/tests/rates_orch.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b5b5c1d5..199cbec53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Crowdfund, added additional state [(#715)](https://github.com/andromedaprotocol/andromeda-core/pull/715) - - Added optional config for Send in Splitter contracts [(#686)](https://github.com/andromedaprotocol/andromeda-core/pull/686) - Added CW20 suppport in Splitter contracts [(#703)](https://github.com/andromedaprotocol/andromeda-core/pull/703) - Matrix ADO [(#539)](https://github.com/andromedaprotocol/andromeda-core/pull/539) - Added Distance ADO [(#570)](https://github.com/andromedaprotocol/andromeda-core/pull/570) +- Rates: Handle cross-chain recipients [(#671)](https://github.com/andromedaprotocol/andromeda-core/pull/671) ### Changed +- Rates: Limit rates recipient to only one address [(#669)](https://github.com/andromedaprotocol/andromeda-core/pull/669) ### Fixed @@ -92,6 +93,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Validator Staking: Updated according to audit [(#565)](https://github.com/andromedaprotocol/andromeda-core/pull/565) - Conditional Splitter: Change lock_time's type from MillisecondsDuration to Expiry [(#567)](https://github.com/andromedaprotocol/andromeda-core/pull/567) + ### Fixed - Splitter: avoid zero send messages, owner updates lock any time [(#457)](https://github.com/andromedaprotocol/andromeda-core/pull/457) diff --git a/Cargo.lock b/Cargo.lock index b3e206e39..24dd6864e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -868,7 +868,7 @@ dependencies = [ [[package]] name = "andromeda-std" -version = "1.3.4" +version = "1.5.0" dependencies = [ "andromeda-macros", "cosmwasm-schema 1.5.8", diff --git a/contracts/data-storage/andromeda-boolean/src/testing/tests.rs b/contracts/data-storage/andromeda-boolean/src/testing/tests.rs index 060bd136a..c71ea2d6e 100644 --- a/contracts/data-storage/andromeda-boolean/src/testing/tests.rs +++ b/contracts/data-storage/andromeda-boolean/src/testing/tests.rs @@ -51,7 +51,11 @@ fn test_set_value_with_tax() { action: "BooleanSetValue".to_string(), rate: Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![], + recipient: Recipient { + address: AndrAddr::from_string(String::default()), + msg: None, + ibc_recovery_address: None, + }, value: LocalRateValue::Percent(PercentRate { percent: Decimal::one(), }), @@ -71,11 +75,11 @@ fn test_set_value_with_tax() { let rate: Rate = Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string(tax_recipient.to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Flat(coin(20_u128, "uandr")), description: None, }); diff --git a/contracts/data-storage/andromeda-primitive/src/testing/tests.rs b/contracts/data-storage/andromeda-primitive/src/testing/tests.rs index 268c2527e..3c98083ea 100644 --- a/contracts/data-storage/andromeda-primitive/src/testing/tests.rs +++ b/contracts/data-storage/andromeda-primitive/src/testing/tests.rs @@ -72,7 +72,11 @@ fn test_set_value_with_tax() { action: "PrimitiveSetValue".to_string(), rate: Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![], + recipient: Recipient { + address: AndrAddr::from_string(String::default()), + msg: None, + ibc_recovery_address: None, + }, value: LocalRateValue::Percent(PercentRate { percent: Decimal::one(), }), @@ -93,7 +97,11 @@ fn test_set_value_with_tax() { // Make sure sender is set as recipient when the recipients vector is empty let rate: Rate = Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![], + recipient: Recipient { + address: AndrAddr::from_string("creator".to_string()), + msg: None, + ibc_recovery_address: None, + }, value: LocalRateValue::Flat(coin(20_u128, MOCK_CW20_CONTRACT)), description: None, }); @@ -111,7 +119,7 @@ fn test_set_value_with_tax() { queried_rates.unwrap(), Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient::new(AndrAddr::from_string("creator"), None)], + recipient: Recipient::new(AndrAddr::from_string("creator"), None), value: LocalRateValue::Flat(coin(20_u128, MOCK_CW20_CONTRACT)), description: None, }) @@ -119,11 +127,11 @@ fn test_set_value_with_tax() { let rate: Rate = Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string(tax_recipient.to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Flat(coin(20_u128, "uandr")), description: None, }); diff --git a/contracts/data-storage/andromeda-string-storage/src/testing/tests.rs b/contracts/data-storage/andromeda-string-storage/src/testing/tests.rs index df9cc8b59..2cc801b80 100644 --- a/contracts/data-storage/andromeda-string-storage/src/testing/tests.rs +++ b/contracts/data-storage/andromeda-string-storage/src/testing/tests.rs @@ -62,7 +62,11 @@ fn test_set_value_with_tax() { action: "StringStorageSetValue".to_string(), rate: Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![], + recipient: Recipient { + address: AndrAddr::from_string(String::default()), + msg: None, + ibc_recovery_address: None, + }, value: LocalRateValue::Percent(PercentRate { percent: Decimal::one(), }), @@ -82,11 +86,11 @@ fn test_set_value_with_tax() { let rate: Rate = Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string(tax_recipient.to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Flat(coin(20_u128, "uandr")), description: None, }); diff --git a/contracts/finance/andromeda-splitter/src/contract.rs b/contracts/finance/andromeda-splitter/src/contract.rs index 41875a5bb..151c58394 100644 --- a/contracts/finance/andromeda-splitter/src/contract.rs +++ b/contracts/finance/andromeda-splitter/src/contract.rs @@ -6,7 +6,7 @@ use andromeda_finance::splitter::{ use andromeda_std::{ ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, amp::{messages::AMPPkt, Recipient}, - common::{actions::call_action, encode_binary, expiration::Expiry, Milliseconds}, + common::{actions::call_action, encode_binary, expiration::Expiry}, error::ContractError, }; use andromeda_std::{ado_contract::ADOContract, common::context::ExecuteContext}; diff --git a/contracts/finance/andromeda-splitter/src/mock.rs b/contracts/finance/andromeda-splitter/src/mock.rs index 774d7c206..fc236e845 100644 --- a/contracts/finance/andromeda-splitter/src/mock.rs +++ b/contracts/finance/andromeda-splitter/src/mock.rs @@ -13,6 +13,7 @@ pub struct MockSplitter(Addr); mock_ado!(MockSplitter, ExecuteMsg, QueryMsg); impl MockSplitter { + #[allow(clippy::too_many_arguments)] pub fn instantiate( app: &mut MockApp, code_id: u64, diff --git a/contracts/fungible-tokens/andromeda-cw20/src/testing/tests.rs b/contracts/fungible-tokens/andromeda-cw20/src/testing/tests.rs index 3d3a1e8f6..6bc3115c2 100644 --- a/contracts/fungible-tokens/andromeda-cw20/src/testing/tests.rs +++ b/contracts/fungible-tokens/andromeda-cw20/src/testing/tests.rs @@ -89,11 +89,11 @@ fn test_transfer() { // Set a royalty of 10% to be paid to royalty_recipient let rate = Rate::Local(LocalRate { rate_type: LocalRateType::Deductive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string("royalty_recipient".to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Percent(PercentRate { percent: Decimal::percent(10), }), @@ -195,11 +195,11 @@ fn test_send() { let rate = Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string("rates_recipient".to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Percent(PercentRate { percent: Decimal::percent(10), }), diff --git a/contracts/math/andromeda-point/src/testing/tests.rs b/contracts/math/andromeda-point/src/testing/tests.rs index b051ad75e..578a76280 100644 --- a/contracts/math/andromeda-point/src/testing/tests.rs +++ b/contracts/math/andromeda-point/src/testing/tests.rs @@ -54,7 +54,11 @@ fn test_set_point_with_tax() { action: "PointSetPoint".to_string(), rate: Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![], + recipient: Recipient { + address: AndrAddr::from_string(String::default()), + msg: None, + ibc_recovery_address: None, + }, value: LocalRateValue::Percent(PercentRate { percent: Decimal::one(), }), @@ -74,11 +78,11 @@ fn test_set_point_with_tax() { let rate: Rate = Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string(tax_recipient.to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Flat(coin(20_u128, "uandr")), description: None, }); diff --git a/contracts/modules/andromeda-rates/src/contract.rs b/contracts/modules/andromeda-rates/src/contract.rs index 5df97a64f..c59b4aaac 100644 --- a/contracts/modules/andromeda-rates/src/contract.rs +++ b/contracts/modules/andromeda-rates/src/contract.rs @@ -7,7 +7,6 @@ use andromeda_std::{ InstantiateMsg as BaseInstantiateMsg, MigrateMsg, }, ado_contract::ADOContract, - amp::Recipient, common::{context::ExecuteContext, deduct_funds, encode_binary, Funds}, error::ContractError, }; @@ -31,11 +30,7 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { let action = msg.action; - let mut rate = msg.rate; - - if rate.recipients.is_empty() { - rate.recipients = vec![Recipient::new(info.sender.clone(), None)]; - }; + let rate = msg.rate; RATES.save(deps.storage, &action, &rate)?; @@ -84,7 +79,7 @@ pub fn handle_execute(ctx: ExecuteContext, msg: ExecuteMsg) -> Result Result { let ExecuteContext { deps, info, .. } = ctx; nonpayable(&info)?; @@ -93,13 +88,7 @@ fn execute_set_rate( ADOContract::default().is_contract_owner(deps.storage, info.sender.as_str())?, ContractError::Unauthorized {} ); - // Validate the local rate's value - rate.value.validate(deps.as_ref())?; - - // Set the sender as the recipient in case no recipients were provided - if rate.recipients.is_empty() { - rate.recipients = vec![Recipient::new(info.sender, None)]; - }; + rate.validate(deps.as_ref())?; RATES.save(deps.storage, &action, &rate)?; @@ -170,32 +159,38 @@ pub fn query_deducted_funds( } local_rate.value.validate(deps)?; let fee = calculate_fee(local_rate.value, &coin)?; - for receiver in local_rate.recipients.iter() { - if !local_rate.rate_type.is_additive() { - deduct_funds(&mut leftover_funds, &fee)?; - event = event.add_attribute("deducted", fee.to_string()); - } - event = event.add_attribute( - "payment", - PaymentAttribute { - receiver: receiver.get_addr(), - amount: fee.clone(), - } - .to_string(), - ); - let msg = if is_native { - receiver.generate_direct_msg(&deps, vec![fee.clone()])? - } else { - receiver.generate_msg_cw20( - &deps, - Cw20Coin { - amount: fee.amount, - address: fee.denom.to_string(), - }, - )? - }; - msgs.push(msg); + + if !local_rate.rate_type.is_additive() { + deduct_funds(&mut leftover_funds, &fee)?; + event = event.add_attribute("deducted", fee.to_string()); } + event = event.add_attribute( + "payment", + PaymentAttribute { + receiver: local_rate + .recipient + .address + .get_raw_address(&deps)? + .to_string(), + amount: fee.clone(), + } + .to_string(), + ); + let msg = if is_native { + local_rate + .recipient + .generate_direct_msg(&deps, vec![fee.clone()])? + } else { + local_rate.recipient.generate_msg_cw20( + &deps, + Cw20Coin { + amount: fee.amount, + address: fee.denom.to_string(), + }, + )? + }; + msgs.push(msg); + events.push(event); Ok(RatesResponse { diff --git a/contracts/modules/andromeda-rates/src/testing/tests.rs b/contracts/modules/andromeda-rates/src/testing/tests.rs index 419f17e50..4e7928573 100644 --- a/contracts/modules/andromeda-rates/src/testing/tests.rs +++ b/contracts/modules/andromeda-rates/src/testing/tests.rs @@ -27,11 +27,11 @@ fn test_instantiate_query() { let action = "deposit".to_string(); let rate = LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string("owner".to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Flat(coin(100_u128, "uandr")), description: None, }; @@ -63,11 +63,11 @@ fn test_andr_receive() { let action: String = "deposit".to_string(); let rate = LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string("owner".to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Flat(coin(100_u128, MOCK_UANDR)), description: None, }; @@ -97,11 +97,11 @@ fn test_query_deducted_funds_native() { let payload = encode_binary(&action).unwrap(); let rate = LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string("recipient1".to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Flat(coin(20_u128, MOCK_UANDR)), description: None, }; @@ -153,11 +153,11 @@ fn test_query_deducted_funds_cw20() { let payload = encode_binary(&action).unwrap(); let rate = LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string("recipient1".to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Flat(coin(20_u128, MOCK_CW20_CONTRACT)), description: None, }; diff --git a/contracts/non-fungible-tokens/andromeda-auction/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-auction/src/testing/tests.rs index aaf6e0647..c09994ff6 100644 --- a/contracts/non-fungible-tokens/andromeda-auction/src/testing/tests.rs +++ b/contracts/non-fungible-tokens/andromeda-auction/src/testing/tests.rs @@ -1293,11 +1293,11 @@ fn execute_claim_with_tax() { let rate: Rate = Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string(tax_recipient.to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Flat(coin(20_u128, "uusd")), description: None, }); @@ -1470,11 +1470,11 @@ fn execute_claim_with_royalty() { let rate: Rate = Rate::Local(LocalRate { rate_type: LocalRateType::Deductive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string(royalty_recipient.to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Flat(coin(20_u128, "uusd")), description: None, }); @@ -1632,11 +1632,11 @@ fn execute_claim_cw20_with_tax() { let tax_recipient = "tax_recipient"; let rate: Rate = Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string(tax_recipient.to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Percent(PercentRate { percent: Decimal::percent(20), }), diff --git a/contracts/non-fungible-tokens/andromeda-marketplace/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-marketplace/src/testing/tests.rs index c93389800..377976c85 100644 --- a/contracts/non-fungible-tokens/andromeda-marketplace/src/testing/tests.rs +++ b/contracts/non-fungible-tokens/andromeda-marketplace/src/testing/tests.rs @@ -780,11 +780,11 @@ fn test_execute_buy_with_tax_and_royalty_insufficient_funds() { let rate = Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string("tax_recipient".to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Percent(PercentRate { percent: Decimal::percent(50), }), @@ -836,11 +836,11 @@ fn test_execute_buy_with_tax_and_royalty_insufficient_funds_cw20() { let rate = Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string("tax_recipient".to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Percent(PercentRate { percent: Decimal::percent(50), }), @@ -913,11 +913,11 @@ fn test_execute_buy_with_tax_and_royalty_works() { let rate = Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string("tax_recipient".to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Percent(PercentRate { percent: Decimal::percent(50), }), diff --git a/contracts/os/andromeda-kernel/src/execute.rs b/contracts/os/andromeda-kernel/src/execute.rs index dedaebcec..25b329572 100644 --- a/contracts/os/andromeda-kernel/src/execute.rs +++ b/contracts/os/andromeda-kernel/src/execute.rs @@ -774,7 +774,6 @@ impl MsgHandler { gas_limit: None, reply_on: cosmwasm_std::ReplyOn::Always, }); - Ok(resp .add_attribute(format!("method:{sequence}"), "execute_transfer_funds") .add_attribute(format!("channel:{sequence}"), channel) diff --git a/packages/std/Cargo.toml b/packages/std/Cargo.toml index 175bfa540..78e197fca 100644 --- a/packages/std/Cargo.toml +++ b/packages/std/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "andromeda-std" -version = "1.3.4" +version = "1.5.0" edition = "2021" rust-version = "1.75.0" description = "The standard library for creating an Andromeda Digital Object" diff --git a/packages/std/src/ado_base/rates.rs b/packages/std/src/ado_base/rates.rs index f9241c222..0c69bcc46 100644 --- a/packages/std/src/ado_base/rates.rs +++ b/packages/std/src/ado_base/rates.rs @@ -1,14 +1,17 @@ use crate::{ ado_contract::ADOContract, - amp::{AndrAddr, Recipient}, + amp::{ + messages::{AMPMsg, AMPMsgConfig}, + AndrAddr, Recipient, + }, common::{deduct_funds, denom::validate_native_denom, Funds}, error::ContractError, os::{adodb::ADOVersion, aos_querier::AOSQuerier}, }; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - ensure, has_coins, to_json_binary, Coin, Decimal, Deps, Event, Fraction, QueryRequest, SubMsg, - WasmQuery, + ensure, has_coins, to_json_binary, Addr, Coin, Decimal, Deps, Event, Fraction, QueryRequest, + ReplyOn, SubMsg, WasmMsg, WasmQuery, }; use cw20::{Cw20Coin, Cw20QueryMsg, TokenInfoResponse}; @@ -80,6 +83,20 @@ pub enum LocalRateValue { Flat(Coin), } impl LocalRateValue { + /// Used to see if the denom is potentially a cw20 address, if it is, it cannot be paired with a cross-chain recipient + pub fn is_valid_address(&self, deps: Deps) -> Result { + match self { + LocalRateValue::Flat(coin) => { + let denom = coin.denom.clone(); + let is_valid_address = deps.api.addr_validate(denom.as_str()); + match is_valid_address { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } + } + LocalRateValue::Percent(_) => Ok(false), + } + } pub fn validate(&self, deps: Deps) -> Result<(), ContractError> { match self { // If it's a coin, make sure it's non-zero @@ -127,10 +144,22 @@ impl LocalRateValue { #[cw_serde] pub struct LocalRate { pub rate_type: LocalRateType, - pub recipients: Vec, + pub recipient: Recipient, pub value: LocalRateValue, pub description: Option, } +impl LocalRate { + pub fn validate(&self, deps: Deps) -> Result<(), ContractError> { + if self.recipient.is_cross_chain() { + ensure!( + !self.value.is_valid_address(deps)?, + ContractError::InvalidCw20CrossChainRate {} + ); + } + self.value.validate(deps)?; + Ok(()) + } +} // Created this because of the very complex return value warning. type LocalRateResponse = (Vec, Vec, Vec); @@ -151,33 +180,65 @@ impl LocalRate { event = event.add_attribute("description", desc); } let fee = calculate_fee(self.value.clone(), &coin)?; - for receiver in self.recipients.iter() { - // If the rate type is deductive - if !self.rate_type.is_additive() { - deduct_funds(&mut leftover_funds, &fee)?; - event = event.add_attribute("deducted", fee.to_string()); + + // If the rate type is deductive + if !self.rate_type.is_additive() { + deduct_funds(&mut leftover_funds, &fee)?; + event = event.add_attribute("deducted", fee.to_string()); + } + event = event.add_attribute( + "payment", + PaymentAttribute { + receiver: self + .recipient + .address + .get_raw_address(&deps) + .unwrap_or(Addr::unchecked(self.recipient.address.to_string())) + .to_string(), + amount: fee.clone(), } - event = event.add_attribute( - "payment", - PaymentAttribute { - receiver: receiver.get_addr(), - amount: fee.clone(), - } - .to_string(), - ); - let msg = if is_native { - receiver.generate_direct_msg(&deps, vec![fee.clone()])? + .to_string(), + ); + let msg = if self.recipient.is_cross_chain() { + ensure!(is_native, ContractError::InvalidCw20CrossChainRate {}); + // Create a cross chain message to be sent to the kernel + let kernel_address = ADOContract::default().get_kernel_address(deps.storage)?; + let kernel_msg = crate::os::kernel::ExecuteMsg::Send { + message: AMPMsg { + recipient: self.recipient.address.clone(), + message: self.recipient.msg.clone().unwrap_or_default(), + funds: vec![fee.clone()], + config: AMPMsgConfig { + reply_on: ReplyOn::Always, + exit_at_error: false, + gas_limit: None, + direct: true, + ibc_config: None, + }, + }, + }; + SubMsg::new(WasmMsg::Execute { + contract_addr: kernel_address.to_string(), + msg: to_json_binary(&kernel_msg)?, + funds: vec![fee.clone()], + }) + } else { + if is_native { + self.recipient + .generate_direct_msg(&deps, vec![fee.clone()])? } else { - receiver.generate_msg_cw20( + self.recipient.generate_msg_cw20( &deps, Cw20Coin { amount: fee.amount, address: fee.denom.to_string(), }, )? - }; - msgs.push(msg); - } + } + }; + + msgs.push(msg); + events.push(event); Ok((msgs, events, leftover_funds)) } @@ -213,8 +274,7 @@ impl Rate { } } Rate::Local(local_rate) => { - // Validate the local rate value - local_rate.value.validate(deps)?; + local_rate.validate(deps)?; Ok(()) } } diff --git a/packages/std/src/ado_contract/execute.rs b/packages/std/src/ado_contract/execute.rs index 00ce0b2db..7988cb8ed 100644 --- a/packages/std/src/ado_contract/execute.rs +++ b/packages/std/src/ado_contract/execute.rs @@ -1,3 +1,11 @@ +#[cfg(feature = "rates")] +use { + crate::ado_base::rates::{LocalRate, Rate}, + crate::amp::Recipient, + cw_storage_plus::Path, + std::ops::Deref, +}; + use crate::ado_contract::ADOContract; use crate::amp::addresses::AndrAddr; use crate::amp::messages::AMPPkt; @@ -14,6 +22,7 @@ use cosmwasm_std::{ DepsMut, Env, MessageInfo, QuerierWrapper, Response, StdError, Storage, SubMsg, WasmMsg, }; use cw2::{get_contract_version, set_contract_version}; + use semver::Version; use serde::de::DeserializeOwned; use serde::Serialize; @@ -145,6 +154,48 @@ impl<'a> ADOContract<'a> { } ); + #[cfg(feature = "rates")] + { + let all_rates = self.get_all_rates(deps.as_ref())?; + for (action, rate) in all_rates.all_rates { + match rate { + Rate::Local(local_rate) => { + // Remove if recipient is in old Vec format + if from_json::>(&to_json_binary(&local_rate.recipient)?) + .is_ok() + { + // Clearing all rates assuming that if one needs to be removed then all of them should be removed + self.rates.clear(deps.storage); + } + // One iteration is enough since the rates are either all valid or invalid + break; + } + Rate::Contract(andr_addr) => { + let contract_addr = andr_addr.get_raw_address(&deps.as_ref())?; + let key_path: Path> = + Path::new("rates".as_bytes(), &[action.as_bytes()]); + + if let Some(remote_rate) = deps + .querier + .query_wasm_raw(&contract_addr, key_path.deref())? + { + // Remove if remote rate's recipient is in old Vec format + if let Ok(local_rate) = from_json::(&remote_rate) { + if from_json::>(&to_json_binary( + &local_rate.recipient, + )?) + .is_ok() + { + self.rates.clear(deps.storage); + } + break; + } + } + } + } + } + } + set_contract_version(deps.storage, contract_name, contract_version)?; Ok(Response::default()) } diff --git a/packages/std/src/ado_contract/rates.rs b/packages/std/src/ado_contract/rates.rs index e8c47e584..eac9e69c5 100644 --- a/packages/std/src/ado_contract/rates.rs +++ b/packages/std/src/ado_contract/rates.rs @@ -1,5 +1,4 @@ use crate::ado_base::rates::{AllRatesResponse, Rate, RatesMessage, RatesResponse}; -use crate::amp::Recipient; use crate::common::{context::ExecuteContext, Funds}; use crate::error::ContractError; use crate::os::aos_querier::AOSQuerier; @@ -39,7 +38,7 @@ impl<'a> ADOContract<'a> { &self, ctx: ExecuteContext, action: impl Into, - mut rate: Rate, + rate: Rate, ) -> Result { ensure!( Self::is_contract_owner(self, ctx.deps.storage, ctx.info.sender.as_str())?, @@ -49,17 +48,6 @@ impl<'a> ADOContract<'a> { // Validate rates rate.validate_rate(ctx.deps.as_ref())?; - let rate = match rate { - Rate::Local(ref mut local_rate) => { - if local_rate.recipients.is_empty() { - local_rate.recipients = vec![Recipient::new(ctx.info.sender, None)]; - Rate::Local(local_rate.clone()) - } else { - rate - } - } - Rate::Contract(_) => rate, - }; self.set_rates(ctx.deps.storage, action, rate)?; Ok(Response::default().add_attributes(vec![("action", "set_rates")])) @@ -207,11 +195,11 @@ mod tests { let expected_rate = Rate::Local(LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient { + recipient: Recipient { address: AndrAddr::from_string("owner".to_string()), msg: None, ibc_recovery_address: None, - }], + }, value: LocalRateValue::Flat(coin(100_u128, "uandr")), description: None, }); diff --git a/packages/std/src/amp/recipient.rs b/packages/std/src/amp/recipient.rs index 8797cbcbd..520d306fa 100644 --- a/packages/std/src/amp/recipient.rs +++ b/packages/std/src/amp/recipient.rs @@ -58,6 +58,14 @@ impl Recipient { self.msg.clone() } + pub fn is_cross_chain(&self) -> bool { + let protocol = self.address.get_protocol(); + match protocol { + Some("ibc") => true, + _ => false, + } + } + /// Generates a direct sub message for the given recipient. pub fn generate_direct_msg( &self, diff --git a/packages/std/src/error.rs b/packages/std/src/error.rs index 03b01bfd2..c76346399 100644 --- a/packages/std/src/error.rs +++ b/packages/std/src/error.rs @@ -61,6 +61,9 @@ pub enum ContractError { #[error("NoDenomInfoProvided")] NoDenomInfoProvided {}, + #[error("Cannot assign cw20 rate to cross-chain recipient")] + InvalidCw20CrossChainRate {}, + #[error("InvalidAmount: {msg}")] InvalidAmount { msg: String }, diff --git a/tests-integration/tests/auction_app.rs b/tests-integration/tests/auction_app.rs index c277ad103..1e5d47345 100644 --- a/tests-integration/tests/auction_app.rs +++ b/tests-integration/tests/auction_app.rs @@ -11,7 +11,7 @@ use andromeda_finance::splitter::AddressPercent; use andromeda_non_fungible_tokens::auction::{AuctionStateResponse, Cw20HookMsg}; use andromeda_rates::mock::mock_andromeda_rates; use andromeda_splitter::mock::{ - mock_andromeda_splitter, mock_splitter_instantiate_msg, mock_splitter_send_msg, + mock_andromeda_splitter, mock_splitter_instantiate_msg, mock_splitter_send_msg, MockSplitter, }; use andromeda_std::{ ado_base::{ @@ -79,8 +79,34 @@ fn test_auction_app_modules() { to_json_binary(&auction_init_msg).unwrap(), ); + let splitter_init_msg = mock_splitter_instantiate_msg( + vec![ + AddressPercent { + recipient: Recipient::new(recipient_one, None), + percent: Decimal::percent(50), + }, + AddressPercent { + recipient: Recipient::new(recipient_two, None), + percent: Decimal::percent(50), + }, + ], + andr.kernel.addr().to_string(), + None, + None, + None, + ); + let splitter_component = AppComponent::new( + "splitter".to_string(), + "splitter".to_string(), + to_json_binary(&splitter_init_msg).unwrap(), + ); + // Create App - let app_components = vec![cw721_component.clone(), auction_component.clone()]; + let app_components = vec![ + cw721_component.clone(), + auction_component.clone(), + splitter_component.clone(), + ]; let app = MockAppContract::instantiate( andr.get_code_id(&mut router, "app-contract"), owner, @@ -109,6 +135,9 @@ fn test_auction_app_modules() { // Send Token to Auction let auction: MockAuction = app.query_ado_by_component_name(&router, auction_component.name); + // Set up splitter to send funds to recipient one and two + let splitter: MockSplitter = app.query_ado_by_component_name(&router, splitter_component.name); + // Set rates to auction auction .execute_add_rate( @@ -117,12 +146,12 @@ fn test_auction_app_modules() { "Claim".to_string(), Rate::Local(LocalRate { rate_type: LocalRateType::Deductive, - recipients: vec![ - Recipient::new(recipient_one, None), - Recipient::new(recipient_two, None), - ], + recipient: Recipient::new( + splitter.addr(), + Some(to_json_binary(&mock_splitter_send_msg(None)).unwrap()), + ), value: LocalRateValue::Percent(PercentRate { - percent: Decimal::percent(25), + percent: Decimal::percent(50), }), description: None, }), @@ -408,6 +437,7 @@ fn test_auction_app_recipient() { time: start_time.plus_milliseconds(Milliseconds(1000)).into(), chain_id: router.block_info().chain_id, }); + auction .execute_claim_auction( &mut router, diff --git a/tests-integration/tests/cw20_app.rs b/tests-integration/tests/cw20_app.rs index 9fe6fb8e7..bafd13123 100644 --- a/tests-integration/tests/cw20_app.rs +++ b/tests-integration/tests/cw20_app.rs @@ -33,7 +33,6 @@ fn test_cw20_with_rates() { let buyer_one = andr.get_wallet("buyer_one"); let buyer_two = andr.get_wallet("buyer_two"); let recipient_one = andr.get_wallet("recipient_one"); - let recipient_two = andr.get_wallet("recipient_two"); // Generate App Components let initial_balances = vec![ @@ -100,10 +99,7 @@ fn test_cw20_with_rates() { "TransferFrom".to_string(), Rate::Local(LocalRate { rate_type: LocalRateType::Deductive, - recipients: vec![ - Recipient::new(recipient_one, None), - Recipient::new(recipient_two, None), - ], + recipient: Recipient::new(recipient_one, None), value: LocalRateValue::Percent(PercentRate { percent: Decimal::percent(10), }), @@ -134,13 +130,10 @@ fn test_cw20_with_rates() { let recip_one_balance = cw20.query_balance(&router, recipient_one); assert_eq!(Uint128::one(), recip_one_balance); - let recip_two_balance = cw20.query_balance(&router, recipient_two); - assert_eq!(Uint128::one(), recip_two_balance); - let buyer_two_balance = cw20.query_balance(&router, buyer_two); assert_eq!( buyer_two_original_balance - .checked_add(Uint128::new(8)) + .checked_add(Uint128::new(9)) .unwrap(), buyer_two_balance ); diff --git a/tests-integration/tests/kernel_orch.rs b/tests-integration/tests/kernel_orch.rs index 4a5171423..48f19ad1c 100644 --- a/tests-integration/tests/kernel_orch.rs +++ b/tests-integration/tests/kernel_orch.rs @@ -1,4 +1,5 @@ use andromeda_adodb::ADODBContract; +use andromeda_auction::{mock::mock_start_auction, AuctionContract}; use andromeda_counter::CounterContract; use andromeda_economics::EconomicsContract; use andromeda_finance::splitter::{ @@ -9,14 +10,17 @@ use andromeda_math::counter::{ InstantiateMsg as CounterInstantiateMsg, State, }; +use andromeda_cw721::CW721Contract; use andromeda_kernel::KernelContract; +use andromeda_non_fungible_tokens::cw721::TokenExtension; use andromeda_splitter::SplitterContract; use andromeda_std::{ + ado_base::rates::{LocalRate, LocalRateType, LocalRateValue, PercentRate, Rate, RatesMessage}, amp::{ messages::{AMPMsg, AMPMsgConfig}, AndrAddr, Recipient, }, - common::Milliseconds, + common::{denom::Asset, expiration::Expiry, Milliseconds}, os::{ self, kernel::{AcknowledgementMsg, ExecuteMsg, InstantiateMsg, SendMessageWithFundsResponse}, @@ -24,7 +28,7 @@ use andromeda_std::{ }; use andromeda_vfs::VFSContract; use cosmwasm_std::{ - to_json_binary, Addr, Binary, Decimal, IbcAcknowledgement, IbcEndpoint, IbcPacket, + coin, to_json_binary, Addr, Binary, Decimal, IbcAcknowledgement, IbcEndpoint, IbcPacket, IbcPacketAckMsg, IbcTimeout, Timestamp, Uint128, }; use cw_orch::prelude::*; @@ -586,6 +590,7 @@ fn test_kernel_ibc_execute_only_multi_hop() { fn test_kernel_ibc_funds_only() { // Here `juno-1` is the chain-id and `juno` is the address prefix for this chain let sender = Addr::unchecked("sender_for_all_chains").into_string(); + let buyer = Addr::unchecked("buyer").into_string(); let interchain = MockInterchainEnv::new(vec![ ("juno", &sender), @@ -596,12 +601,17 @@ fn test_kernel_ibc_funds_only() { let juno = interchain.get_chain("juno").unwrap(); let osmosis = interchain.get_chain("osmosis").unwrap(); - juno.set_balance(sender.clone(), vec![Coin::new(100000000000000, "juno")]) .unwrap(); + juno.set_balance(buyer.clone(), vec![Coin::new(100000000000000, "juno")]) + .unwrap(); let kernel_juno = KernelContract::new(juno.clone()); let vfs_juno = VFSContract::new(juno.clone()); + let adodb_juno = ADODBContract::new(juno.clone()); + let economics_juno = EconomicsContract::new(juno.clone()); + let mut auction_juno = AuctionContract::new(juno.clone()); + let cw721_juno = CW721Contract::new(juno.clone()); let kernel_osmosis = KernelContract::new(osmosis.clone()); let counter_osmosis = CounterContract::new(osmosis.clone()); let vfs_osmosis = VFSContract::new(osmosis.clone()); @@ -610,6 +620,11 @@ fn test_kernel_ibc_funds_only() { kernel_juno.upload().unwrap(); vfs_juno.upload().unwrap(); + adodb_juno.upload().unwrap(); + economics_juno.upload().unwrap(); + auction_juno.upload().unwrap(); + cw721_juno.upload().unwrap(); + kernel_osmosis.upload().unwrap(); counter_osmosis.upload().unwrap(); vfs_osmosis.upload().unwrap(); @@ -703,6 +718,51 @@ fn test_kernel_ibc_funds_only() { ) .unwrap(); + adodb_juno + .instantiate( + &os::adodb::InstantiateMsg { + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + adodb_juno + .execute( + &os::adodb::ExecuteMsg::Publish { + code_id: 4, + ado_type: "economics".to_string(), + action_fees: None, + version: "1.1.1".to_string(), + publisher: None, + }, + None, + ) + .unwrap(); + + economics_juno + .instantiate( + &os::economics::InstantiateMsg { + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + kernel_juno + .execute( + &ExecuteMsg::UpsertKeyAddress { + key: "economics".to_string(), + value: economics_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + adodb_osmosis .instantiate( &os::adodb::InstantiateMsg { @@ -727,6 +787,19 @@ fn test_kernel_ibc_funds_only() { ) .unwrap(); + adodb_osmosis + .execute( + &os::adodb::ExecuteMsg::Publish { + code_id: 6, + ado_type: "economics".to_string(), + action_fees: None, + version: "1.1.1".to_string(), + publisher: None, + }, + None, + ) + .unwrap(); + kernel_juno .execute( &ExecuteMsg::UpsertKeyAddress { @@ -737,6 +810,16 @@ fn test_kernel_ibc_funds_only() { ) .unwrap(); + kernel_juno + .execute( + &ExecuteMsg::UpsertKeyAddress { + key: "adodb".to_string(), + value: adodb_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + kernel_osmosis .execute( &ExecuteMsg::UpsertKeyAddress { @@ -782,6 +865,10 @@ fn test_kernel_ibc_funds_only() { .unwrap(); let recipient = "osmo1qzskhrca90qy2yjjxqzq4yajy842x7c50xq33d"; + println!( + "osmosis kernel address: {}", + kernel_osmosis.address().unwrap() + ); let kernel_juno_send_request = kernel_juno .execute( @@ -836,7 +923,7 @@ fn test_kernel_ibc_funds_only() { .execute( &ExecuteMsg::UpsertKeyAddress { key: "trigger_key".to_string(), - value: sender, + value: sender.clone(), }, None, ) @@ -892,6 +979,182 @@ fn test_kernel_ibc_funds_only() { // There was a decode error or the packet timed out // Else the packet timed-out, you may have a relayer error or something is wrong in your application }; + + // Set up cross chain rates recipient + auction_juno + .instantiate( + &andromeda_non_fungible_tokens::auction::InstantiateMsg { + authorized_token_addresses: None, + authorized_cw20_addresses: None, + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + cw721_juno + .instantiate( + &andromeda_non_fungible_tokens::cw721::InstantiateMsg { + name: "test tokens".to_string(), + symbol: "TT".to_string(), + minter: AndrAddr::from_string(sender.clone()), + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + auction_juno + .execute( + &andromeda_non_fungible_tokens::auction::ExecuteMsg::Rates(RatesMessage::SetRate { + action: "Claim".to_string(), + rate: Rate::Local(LocalRate { + rate_type: LocalRateType::Deductive, + recipient: Recipient::new( + AndrAddr::from_string(format!("ibc://osmosis/{}", recipient)), + None, + ), + value: LocalRateValue::Percent(PercentRate { + percent: Decimal::percent(50), + }), + description: None, + }), + }), + None, + ) + .unwrap(); + + cw721_juno + .execute( + &andromeda_non_fungible_tokens::cw721::ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: sender.clone(), + token_uri: None, + extension: TokenExtension::default(), + }, + None, + ) + .unwrap(); + + let start_time = Milliseconds::from_nanos(juno.block_info().unwrap().time.nanos()); + let receive_msg = mock_start_auction( + None, + Expiry::AtTime(start_time.plus_milliseconds(Milliseconds(10000))), + None, + Asset::NativeToken("juno".to_string()), + None, + None, + None, + None, + ); + cw721_juno + .execute( + &andromeda_non_fungible_tokens::cw721::ExecuteMsg::SendNft { + contract: AndrAddr::from_string(auction_juno.address().unwrap()), + token_id: "1".to_string(), + msg: to_json_binary(&receive_msg).unwrap(), + }, + None, + ) + .unwrap(); + juno.wait_seconds(1).unwrap(); + + auction_juno.set_sender(&Addr::unchecked(buyer.clone())); + auction_juno + .execute( + &andromeda_non_fungible_tokens::auction::ExecuteMsg::PlaceBid { + token_id: "1".to_string(), + token_address: cw721_juno.address().unwrap().into_string(), + }, + Some(&[coin(50, "juno")]), + ) + .unwrap(); + juno.next_block().unwrap(); + juno.next_block().unwrap(); + + // Claim + let claim_request = auction_juno + .execute( + &andromeda_non_fungible_tokens::auction::ExecuteMsg::Claim { + token_id: "1".to_string(), + token_address: cw721_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + let packet_lifetime = interchain.await_packets("juno", claim_request).unwrap(); + + // For testing a successful outcome of the first packet sent out in the tx, you can use: + if let IbcPacketOutcome::Success { .. } = &packet_lifetime.packets[0].outcome { + // Packet has been successfully acknowledged and decoded, the transaction has gone through correctly + + // Check recipient balance after trigger execute msg + let balances = osmosis + .query_all_balances(kernel_osmosis.address().unwrap()) + .unwrap(); + assert_eq!(balances.len(), 1); + assert_eq!(balances[0].denom, ibc_denom); + assert_eq!(balances[0].amount.u128(), 25); + } else { + panic!("packet timed out"); + // There was a decode error or the packet timed out + // Else the packet timed-out, you may have a relayer error or something is wrong in your application + }; + + // Construct an Execute msg from the kernel on juno inteded for the splitter on osmosis + let kernel_juno_trigger_request = kernel_juno + .execute( + &ExecuteMsg::TriggerRelay { + packet_sequence: "2".to_string(), + packet_ack_msg: IbcPacketAckMsg::new( + IbcAcknowledgement::new( + to_json_binary(&AcknowledgementMsg::::Ok( + SendMessageWithFundsResponse {}, + )) + .unwrap(), + ), + IbcPacket::new( + Binary::default(), + IbcEndpoint { + port_id: "port_id".to_string(), + channel_id: "channel_id".to_string(), + }, + IbcEndpoint { + port_id: "port_id".to_string(), + channel_id: "channel_id".to_string(), + }, + 1, + IbcTimeout::with_timestamp(Timestamp::from_seconds(1)), + ), + Addr::unchecked("relayer"), + ), + }, + None, + ) + .unwrap(); + + let packet_lifetime = interchain + .await_packets("juno", kernel_juno_trigger_request) + .unwrap(); + + // For testing a successful outcome of the first packet sent out in the tx, you can use: + if let IbcPacketOutcome::Success { .. } = &packet_lifetime.packets[0].outcome { + // Packet has been successfully acknowledged and decoded, the transaction has gone through correctly + + // Check recipient balance after trigger execute msg + let balances = osmosis.query_all_balances(recipient).unwrap(); + assert_eq!(balances.len(), 1); + assert_eq!(balances[0].denom, ibc_denom); + assert_eq!(balances[0].amount.u128(), 100 + 25); + } else { + panic!("packet timed out"); + // There was a decode error or the packet timed out + // Else the packet timed-out, you may have a relayer error or something is wrong in your application + }; } #[test] diff --git a/tests-integration/tests/marketplace_app.rs b/tests-integration/tests/marketplace_app.rs index b3c568831..da35fb728 100644 --- a/tests-integration/tests/marketplace_app.rs +++ b/tests-integration/tests/marketplace_app.rs @@ -68,7 +68,7 @@ fn test_marketplace_app() { // The sale recipient will not receive any funds because they're all going to the royalty recipient let local_rate = LocalRate { rate_type: LocalRateType::Deductive, - recipients: vec![Recipient::from_string(rates_receiver.to_string())], + recipient: Recipient::from_string(rates_receiver.to_string()), value: LocalRateValue::Flat(coin(100, "uandr")), description: None, }; @@ -568,7 +568,7 @@ fn test_marketplace_app_cw20_restricted() { let local_rate = LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient::from_string(rates_receiver.to_string())], + recipient: Recipient::from_string(rates_receiver.to_string()), // This is the cw20's address value: LocalRateValue::Flat(coin(100, cw20.addr().to_string())), description: None, @@ -797,7 +797,7 @@ fn test_marketplace_app_cw20_unrestricted() { // set rates for the second cw20 later let local_rate = LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient::from_string(rates_receiver.to_string())], + recipient: Recipient::from_string(rates_receiver.to_string()), // This is the cw20's address value: LocalRateValue::Percent(PercentRate { percent: Decimal::percent(20), @@ -919,7 +919,7 @@ fn test_marketplace_app_cw20_unrestricted() { .unwrap(); let _local_rate2 = LocalRate { rate_type: LocalRateType::Additive, - recipients: vec![Recipient::from_string(rates_receiver.to_string())], + recipient: Recipient::from_string(rates_receiver.to_string()), // This is the cw20's address value: LocalRateValue::Flat(coin( 100, diff --git a/tests-integration/tests/primitive.rs b/tests-integration/tests/primitive.rs index 8b54bd465..0d39d6488 100644 --- a/tests-integration/tests/primitive.rs +++ b/tests-integration/tests/primitive.rs @@ -84,7 +84,7 @@ fn test_primitive() { "PrimitiveSetValue".to_string(), Rate::Local(LocalRate { rate_type: LocalRateType::Deductive, - recipients: vec![Recipient::new(recipient_one, None)], + recipient: Recipient::new(recipient_one, None), value: LocalRateValue::Percent(PercentRate { percent: Decimal::percent(25), }), @@ -104,7 +104,7 @@ fn test_primitive() { "SetValue".to_string(), Rate::Local(LocalRate { rate_type: LocalRateType::Deductive, - recipients: vec![Recipient::new(recipient_one, None)], + recipient: Recipient::new(recipient_one, None), value: LocalRateValue::Flat(coin(10_u128, "uandr")), description: None, }), diff --git a/tests-integration/tests/rates_orch.rs b/tests-integration/tests/rates_orch.rs new file mode 100644 index 000000000..57904e2e7 --- /dev/null +++ b/tests-integration/tests/rates_orch.rs @@ -0,0 +1,221 @@ +use andromeda_adodb::ADODBContract; +use andromeda_economics::EconomicsContract; +use andromeda_kernel::KernelContract; +use andromeda_marketplace::MarketplaceContract; +use andromeda_modules::rates::InstantiateMsg as RatesInstantiateMsg; +use andromeda_non_fungible_tokens::marketplace::{ + ExecuteMsg, InstantiateMsg as MarketplaceInstantiateMsg, QueryMsg, +}; +use andromeda_rates::RatesContract; +use andromeda_std::{ + ado_base::{ + rates::{AllRatesResponse, LocalRate, LocalRateType, PercentRate}, + MigrateMsg, + }, + amp::{AndrAddr, Recipient}, + os::{ + self, + kernel::{ExecuteMsg as KernelExecuteMsg, InstantiateMsg as KernelInstantiateMsg}, + }, +}; +use andromeda_vfs::VFSContract; +use cosmwasm_std::{Addr, Decimal}; +use cw_orch::prelude::*; +use cw_orch_interchain::prelude::*; + +#[test] +fn test_marketplace_migration() { + // Here `juno-1` is the chain-id and `juno` is the address prefix for this chain + let sender = Addr::unchecked("sender_for_all_chains"); + let interchain = MockInterchainEnv::new(vec![("juno", &sender.clone().into_string())]); + let juno = interchain.get_chain("juno").unwrap(); + + juno.set_balance( + sender.clone().into_string().clone(), + vec![Coin::new(100000000000000, "juno")], + ) + .unwrap(); + + let marketplace_juno = MarketplaceContract::new(juno.clone()); + let kernel_juno = KernelContract::new(juno.clone()); + let rates_juno = RatesContract::new(juno.clone()); + let vfs_juno = VFSContract::new(juno.clone()); + let adodb_juno = ADODBContract::new(juno.clone()); + let economics_juno = EconomicsContract::new(juno.clone()); + + marketplace_juno.upload().unwrap(); + kernel_juno.upload().unwrap(); + rates_juno.upload().unwrap(); + vfs_juno.upload().unwrap(); + adodb_juno.upload().unwrap(); + economics_juno.upload().unwrap(); + + let kernel_init_msg = &KernelInstantiateMsg { + owner: None, + chain_name: "juno".to_string(), + }; + kernel_juno + .instantiate(kernel_init_msg, None, None) + .unwrap(); + + vfs_juno + .instantiate( + &os::vfs::InstantiateMsg { + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + kernel_juno + .execute( + &KernelExecuteMsg::UpsertKeyAddress { + key: "vfs".to_string(), + value: vfs_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + + adodb_juno + .instantiate( + &os::adodb::InstantiateMsg { + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + kernel_juno + .execute( + &KernelExecuteMsg::UpsertKeyAddress { + key: "adodb".to_string(), + value: adodb_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + + adodb_juno + .execute( + &os::adodb::ExecuteMsg::Publish { + code_id: 3, + ado_type: "rates".to_string(), + action_fees: None, + version: "2.0.3".to_string(), + publisher: None, + }, + None, + ) + .unwrap(); + + let rates_init_msg = &RatesInstantiateMsg { + action: "Claim".to_string(), + rate: LocalRate { + rate_type: LocalRateType::Additive, + recipient: Recipient::from_string("recipient".to_string()), + value: andromeda_std::ado_base::rates::LocalRateValue::Percent(PercentRate { + percent: Decimal::one(), + }), + description: None, + }, + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: Some(sender.clone().into_string().clone()), + }; + rates_juno.instantiate(&rates_init_msg, None, None).unwrap(); + + kernel_juno + .execute( + &KernelExecuteMsg::UpsertKeyAddress { + key: "rates".to_string(), + value: rates_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + + let marketplace_init_msg = &MarketplaceInstantiateMsg { + authorized_cw20_addresses: None, + authorized_token_addresses: None, + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: Some(sender.clone().into_string().clone()), + }; + + marketplace_juno + .instantiate(marketplace_init_msg, Some(&sender), None) + .unwrap(); + + adodb_juno + .execute( + &os::adodb::ExecuteMsg::Publish { + code_id: 6, + ado_type: "economics".to_string(), + action_fees: None, + version: "1.1.1".to_string(), + publisher: None, + }, + None, + ) + .unwrap(); + + economics_juno + .instantiate( + &os::economics::InstantiateMsg { + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + kernel_juno + .execute( + &KernelExecuteMsg::UpsertKeyAddress { + key: "economics".to_string(), + value: economics_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + + marketplace_juno + .execute( + &ExecuteMsg::Rates(andromeda_std::ado_base::rates::RatesMessage::SetRate { + action: "Claim".to_string(), + rate: andromeda_std::ado_base::rates::Rate::Contract(AndrAddr::from_string( + rates_juno.address().unwrap().into_string(), + )), + }), + None, + ) + .unwrap(); + + marketplace_juno.upload().unwrap(); + + // Query marketplace rates + let rates_query: AllRatesResponse = marketplace_juno.query(&QueryMsg::AllRates {}).unwrap(); + assert_eq!(rates_query.all_rates.len(), 1); + + marketplace_juno.migrate(&MigrateMsg {}, 7).unwrap(); + + // Adjusting the migrate function for testing purposes will show that the code works as expected. + // Let the code in the migrate function to expect Recipient instead of Vec and the below rates query will return 0 + let rates_query: AllRatesResponse = marketplace_juno.query(&QueryMsg::AllRates {}).unwrap(); + assert_eq!(rates_query.all_rates.len(), 1); + + // vfs_juno + // .instantiate( + // &os::vfs::InstantiateMsg { + // kernel_address: kernel_juno.address().unwrap().into_string(), + // owner: None, + // }, + // None, + // None, + // ) + // .unwrap(); +} From 08552012f4bc11ce6e37238eb43cf56ab4292540 Mon Sep 17 00:00:00 2001 From: Joe Monem <66594578+joemonem@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:14:14 +0200 Subject: [PATCH 4/9] ref: expose query_permissioned_actors in AndromedaQuery (#717) --- CHANGELOG.md | 1 + Cargo.lock | 2 +- packages/std/Cargo.toml | 2 +- packages/std/src/ado_base/mod.rs | 8 ++++++++ packages/std/src/ado_base/permissioning.rs | 5 +++++ packages/std/src/ado_contract/permissioning.rs | 11 ++++++++++- packages/std/src/ado_contract/query.rs | 12 ++++++++++++ 7 files changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 199cbec53..6686c0904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Matrix ADO [(#539)](https://github.com/andromedaprotocol/andromeda-core/pull/539) - Added Distance ADO [(#570)](https://github.com/andromedaprotocol/andromeda-core/pull/570) - Rates: Handle cross-chain recipients [(#671)](https://github.com/andromedaprotocol/andromeda-core/pull/671) +- Permissions: Permissioned Actors in AndromedaQuery [(#717)](https://github.com/andromedaprotocol/andromeda-core/pull/717) ### Changed - Rates: Limit rates recipient to only one address [(#669)](https://github.com/andromedaprotocol/andromeda-core/pull/669) diff --git a/Cargo.lock b/Cargo.lock index 24dd6864e..98d91977b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -868,7 +868,7 @@ dependencies = [ [[package]] name = "andromeda-std" -version = "1.5.0" +version = "1.5.0-b.1" dependencies = [ "andromeda-macros", "cosmwasm-schema 1.5.8", diff --git a/packages/std/Cargo.toml b/packages/std/Cargo.toml index 78e197fca..9ac7e384a 100644 --- a/packages/std/Cargo.toml +++ b/packages/std/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "andromeda-std" -version = "1.5.0" +version = "1.5.0-b.1" edition = "2021" rust-version = "1.75.0" description = "The standard library for creating an Andromeda Digital Object" diff --git a/packages/std/src/ado_base/mod.rs b/packages/std/src/ado_base/mod.rs index 27c5e834a..0e5493e38 100644 --- a/packages/std/src/ado_base/mod.rs +++ b/packages/std/src/ado_base/mod.rs @@ -11,6 +11,7 @@ pub mod version; pub mod withdraw; use crate::amp::{messages::AMPPkt, AndrAddr}; +use crate::common::OrderBy; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Addr; @@ -74,6 +75,13 @@ pub enum AndromedaQuery { }, #[returns(Vec)] PermissionedActions {}, + #[returns(Vec)] + PermissionedActors { + action: String, + limit: Option, + start_after: Option, + order_by: Option, + }, #[cfg(feature = "rates")] #[returns(Option)] diff --git a/packages/std/src/ado_base/permissioning.rs b/packages/std/src/ado_base/permissioning.rs index 49142ee4b..8aca6f656 100644 --- a/packages/std/src/ado_base/permissioning.rs +++ b/packages/std/src/ado_base/permissioning.rs @@ -40,6 +40,11 @@ pub struct PermissionedActionsResponse { pub actions: Vec, } +#[cw_serde] +pub struct PermissionedActorsResponse { + pub actors: Vec, +} + /// An enum to represent a user's permission for an action /// /// - **Blacklisted** - The user cannot perform the action until after the provided expiration diff --git a/packages/std/src/ado_contract/permissioning.rs b/packages/std/src/ado_contract/permissioning.rs index eea1dc70a..421b3365a 100644 --- a/packages/std/src/ado_contract/permissioning.rs +++ b/packages/std/src/ado_contract/permissioning.rs @@ -1178,6 +1178,7 @@ mod tests { contract.owner.save(ctx.deps.storage, &info.sender).unwrap(); let actor = "actor"; + let actor2 = "actor2"; let action = "action"; ADOContract::default() .execute_permission_action(ctx, action) @@ -1190,11 +1191,19 @@ mod tests { Permission::Local(LocalPermission::default()), ) .unwrap(); + ADOContract::set_permission( + deps.as_mut().storage, + action, + actor2, + Permission::Local(LocalPermission::default()), + ) + .unwrap(); let actors = ADOContract::default() .query_permissioned_actors(deps.as_ref(), action, None, None, None) .unwrap(); - assert_eq!(actors.len(), 1); + assert_eq!(actors.len(), 2); assert_eq!(actors[0], actor); + assert_eq!(actors[1], actor2); } } diff --git a/packages/std/src/ado_contract/query.rs b/packages/std/src/ado_contract/query.rs index 4b7b75420..22738bd61 100644 --- a/packages/std/src/ado_contract/query.rs +++ b/packages/std/src/ado_contract/query.rs @@ -55,6 +55,18 @@ impl<'a> ADOContract<'a> { AndromedaQuery::PermissionedActions {} => { encode_binary(&self.query_permissioned_actions(deps)?) } + AndromedaQuery::PermissionedActors { + action, + start_after, + limit, + order_by, + } => encode_binary(&self.query_permissioned_actors( + deps, + action, + start_after, + limit, + order_by, + )?), #[cfg(feature = "rates")] AndromedaQuery::Rates { action } => encode_binary(&self.get_rates(deps, action)?), From 60083f80289cefd9e9f0a81d4543e6384aef7eab Mon Sep 17 00:00:00 2001 From: Mitar Djakovic Date: Mon, 16 Dec 2024 12:30:29 +0200 Subject: [PATCH 5/9] Schema and Form ADO (#591) --- CHANGELOG.md | 1 + Cargo.lock | 39 + Cargo.toml | 2 + .../data-storage/andromeda-form/.cargo/config | 4 + .../data-storage/andromeda-form/Cargo.toml | 45 ++ .../andromeda-form/examples/schema.rs | 10 + .../andromeda-form/src/contract.rs | 180 +++++ .../andromeda-form/src/execute.rs | 436 ++++++++++++ .../andromeda-form/src/interface.rs | 6 + .../data-storage/andromeda-form/src/lib.rs | 14 + .../data-storage/andromeda-form/src/mock.rs | 210 ++++++ .../data-storage/andromeda-form/src/query.rs | 74 ++ .../data-storage/andromeda-form/src/state.rs | 52 ++ .../andromeda-form/src/testing/mock.rs | 198 ++++++ .../src/testing/mock_querier.rs | 110 +++ .../andromeda-form/src/testing/mod.rs | 3 + .../andromeda-form/src/testing/tests.rs | 670 ++++++++++++++++++ .../modules/andromeda-schema/.cargo/config | 4 + contracts/modules/andromeda-schema/Cargo.toml | 35 + .../andromeda-schema/examples/schema.rs | 10 + .../modules/andromeda-schema/src/contract.rs | 100 +++ .../modules/andromeda-schema/src/execute.rs | 59 ++ .../modules/andromeda-schema/src/interface.rs | 6 + contracts/modules/andromeda-schema/src/lib.rs | 14 + .../modules/andromeda-schema/src/mock.rs | 81 +++ .../modules/andromeda-schema/src/query.rs | 226 ++++++ .../modules/andromeda-schema/src/state.rs | 4 + .../andromeda-schema/src/testing/mock.rs | 49 ++ .../andromeda-schema/src/testing/mod.rs | 2 + .../andromeda-schema/src/testing/tests.rs | 220 ++++++ packages/andromeda-data-storage/src/form.rs | 92 +++ packages/andromeda-data-storage/src/lib.rs | 1 + packages/andromeda-modules/src/lib.rs | 1 + packages/andromeda-modules/src/schema.rs | 35 + 34 files changed, 2993 insertions(+) create mode 100644 contracts/data-storage/andromeda-form/.cargo/config create mode 100644 contracts/data-storage/andromeda-form/Cargo.toml create mode 100644 contracts/data-storage/andromeda-form/examples/schema.rs create mode 100644 contracts/data-storage/andromeda-form/src/contract.rs create mode 100644 contracts/data-storage/andromeda-form/src/execute.rs create mode 100644 contracts/data-storage/andromeda-form/src/interface.rs create mode 100644 contracts/data-storage/andromeda-form/src/lib.rs create mode 100644 contracts/data-storage/andromeda-form/src/mock.rs create mode 100644 contracts/data-storage/andromeda-form/src/query.rs create mode 100644 contracts/data-storage/andromeda-form/src/state.rs create mode 100644 contracts/data-storage/andromeda-form/src/testing/mock.rs create mode 100644 contracts/data-storage/andromeda-form/src/testing/mock_querier.rs create mode 100644 contracts/data-storage/andromeda-form/src/testing/mod.rs create mode 100644 contracts/data-storage/andromeda-form/src/testing/tests.rs create mode 100644 contracts/modules/andromeda-schema/.cargo/config create mode 100644 contracts/modules/andromeda-schema/Cargo.toml create mode 100644 contracts/modules/andromeda-schema/examples/schema.rs create mode 100644 contracts/modules/andromeda-schema/src/contract.rs create mode 100644 contracts/modules/andromeda-schema/src/execute.rs create mode 100644 contracts/modules/andromeda-schema/src/interface.rs create mode 100644 contracts/modules/andromeda-schema/src/lib.rs create mode 100644 contracts/modules/andromeda-schema/src/mock.rs create mode 100644 contracts/modules/andromeda-schema/src/query.rs create mode 100644 contracts/modules/andromeda-schema/src/state.rs create mode 100644 contracts/modules/andromeda-schema/src/testing/mock.rs create mode 100644 contracts/modules/andromeda-schema/src/testing/mod.rs create mode 100644 contracts/modules/andromeda-schema/src/testing/tests.rs create mode 100644 packages/andromeda-data-storage/src/form.rs create mode 100644 packages/andromeda-modules/src/schema.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6686c0904..2e017edb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Distance ADO [(#570)](https://github.com/andromedaprotocol/andromeda-core/pull/570) - Rates: Handle cross-chain recipients [(#671)](https://github.com/andromedaprotocol/andromeda-core/pull/671) - Permissions: Permissioned Actors in AndromedaQuery [(#717)](https://github.com/andromedaprotocol/andromeda-core/pull/717) +- Added Schema and Form ADOs [(#591)](https://github.com/andromedaprotocol/andromeda-core/pull/591) ### Changed - Rates: Limit rates recipient to only one address [(#669)](https://github.com/andromedaprotocol/andromeda-core/pull/669) diff --git a/Cargo.lock b/Cargo.lock index 98d91977b..94241575b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -535,6 +535,27 @@ dependencies = [ "cw4", ] +[[package]] +name = "andromeda-form" +version = "0.1.0-beta" +dependencies = [ + "andromeda-data-storage", + "andromeda-modules", + "andromeda-std", + "andromeda-testing", + "cosmwasm-schema 1.5.8", + "cosmwasm-std 1.5.8", + "cw-json", + "cw-multi-test", + "cw-orch", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20 1.1.2", + "serde", + "serde_json", + "test-case", +] + [[package]] name = "andromeda-fungible-tokens" version = "1.0.0" @@ -812,6 +833,24 @@ dependencies = [ "cw20 1.1.2", ] +[[package]] +name = "andromeda-schema" +version = "0.1.0-beta" +dependencies = [ + "andromeda-app", + "andromeda-modules", + "andromeda-std", + "andromeda-testing", + "cosmwasm-schema 1.5.8", + "cosmwasm-std 1.5.8", + "cw-json", + "cw-multi-test", + "cw-orch", + "cw-storage-plus 1.2.0", + "serde_json", + "test-case", +] + [[package]] name = "andromeda-set-amount-splitter" version = "1.2.0-b.1" diff --git a/Cargo.toml b/Cargo.toml index fa8dfc986..605cdb625 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,3 +67,5 @@ cw-multi-test = { version = "1.0.0", features = ["cosmwasm_1_2"] } serde = { version = "1.0.215" } test-case = { version = "3.3.1" } cw-orch = "=0.24.1" +jsonschema-valid = { version = "0.5.2"} +serde_json = { version = "1.0.128" } diff --git a/contracts/data-storage/andromeda-form/.cargo/config b/contracts/data-storage/andromeda-form/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/data-storage/andromeda-form/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/data-storage/andromeda-form/Cargo.toml b/contracts/data-storage/andromeda-form/Cargo.toml new file mode 100644 index 000000000..08890859f --- /dev/null +++ b/contracts/data-storage/andromeda-form/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "andromeda-form" +version = "0.1.0-beta" +authors = ["Mitar Djakovic "] +edition = "2021" +rust-version = "1.75.0" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] +testing = ["cw-multi-test", "andromeda-testing"] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw20 = { workspace = true } +cw-json = { git = "https://github.com/SlayerAnsh/cw-json.git" } +serde_json = { workspace = true } +serde = { workspace = true } +test-case = { workspace = true } + +andromeda-std = { workspace = true, features = ["rates"] } +andromeda-data-storage = { workspace = true } +andromeda-modules = { workspace = true } + + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +cw-orch = { workspace = true } +cw-multi-test = { workspace = true, optional = true } +andromeda-testing = { workspace = true, optional = true } diff --git a/contracts/data-storage/andromeda-form/examples/schema.rs b/contracts/data-storage/andromeda-form/examples/schema.rs new file mode 100644 index 000000000..cb224a660 --- /dev/null +++ b/contracts/data-storage/andromeda-form/examples/schema.rs @@ -0,0 +1,10 @@ +use andromeda_data_storage::form::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::write_api; +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + + } +} diff --git a/contracts/data-storage/andromeda-form/src/contract.rs b/contracts/data-storage/andromeda-form/src/contract.rs new file mode 100644 index 000000000..8cb19e055 --- /dev/null +++ b/contracts/data-storage/andromeda-form/src/contract.rs @@ -0,0 +1,180 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + ensure, Binary, Deps, DepsMut, Env, Event, MessageInfo, Reply, Response, StdError, Uint64, +}; + +use andromeda_data_storage::form::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::{ + ado_base::{ + permissioning::{LocalPermission, Permission}, + InstantiateMsg as BaseInstantiateMsg, MigrateMsg, + }, + ado_contract::ADOContract, + common::{ + context::ExecuteContext, encode_binary, expiration::get_and_validate_start_time, + Milliseconds, + }, + error::ContractError, +}; + +use crate::execute::{handle_execute, milliseconds_from_expiration}; +use crate::query::{ + get_all_submissions, get_form_status, get_schema, get_submission, get_submission_ids, +}; +use crate::state::{Config, CONFIG, SCHEMA_ADO_ADDRESS, SUBMISSION_ID}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:andromeda-form"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const SUBMIT_FORM_ACTION: &str = "submit_form"; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let resp = ADOContract::default().instantiate( + deps.storage, + env.clone(), + deps.api, + &deps.querier, + info, + BaseInstantiateMsg { + ado_type: CONTRACT_NAME.to_string(), + ado_version: CONTRACT_VERSION.to_string(), + kernel_address: msg.kernel_address, + owner: msg.owner, + }, + )?; + + let schema_ado_address = msg.schema_ado_address; + schema_ado_address.validate(deps.api)?; + + SCHEMA_ADO_ADDRESS.save(deps.storage, &schema_ado_address)?; + SUBMISSION_ID.save(deps.storage, &Uint64::zero())?; + + let start_time = match msg.form_config.start_time { + Some(start_time) => Some(milliseconds_from_expiration( + get_and_validate_start_time(&env.clone(), Some(start_time))?.0, + )?), + None => None, + }; + let end_time = match msg.form_config.end_time { + Some(end_time) => { + let time_res = get_and_validate_start_time(&env.clone(), Some(end_time)); + if time_res.is_ok() { + Some(milliseconds_from_expiration(time_res.unwrap().0)?) + } else { + let current_time = Milliseconds::from_nanos(env.block.time.nanos()).milliseconds(); + let current_height = env.block.height; + return Err(ContractError::CustomError { + msg: format!( + "End time in the past. current_time {:?}, current_block {:?}", + current_time, current_height + ), + }); + } + } + None => None, + }; + + if let (Some(start_time), Some(end_time)) = (start_time, end_time) { + ensure!( + end_time.gt(&start_time), + ContractError::StartTimeAfterEndTime {} + ); + } + + let allow_multiple_submissions = msg.form_config.allow_multiple_submissions; + let allow_edit_submission = msg.form_config.allow_edit_submission; + + let config = Config { + start_time, + end_time, + allow_multiple_submissions, + allow_edit_submission, + }; + + CONFIG.save(deps.storage, &config)?; + + if let Some(authorized_addresses_for_submission) = msg.authorized_addresses_for_submission { + if !authorized_addresses_for_submission.is_empty() { + ADOContract::default().permission_action(SUBMIT_FORM_ACTION, deps.storage)?; + } + + for address in authorized_addresses_for_submission { + let addr = address.get_raw_address(&deps.as_ref())?; + ADOContract::set_permission( + deps.storage, + SUBMIT_FORM_ACTION, + addr, + Permission::Local(LocalPermission::Whitelisted(None)), + )?; + } + } + + let mut response = resp.add_event(Event::new("form_instantiated")); + + if let Some(custom_key) = msg.custom_key_for_notifications { + response = response.add_event( + cosmwasm_std::Event::new("custom_key") + .add_attribute("custom_key", custom_key) + .add_attribute("notification_service", "Telegram"), + ); + } + + Ok(response) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let ctx = ExecuteContext::new(deps, info, env); + match msg { + ExecuteMsg::AMPReceive(pkt) => { + ADOContract::default().execute_amp_receive(ctx, pkt, handle_execute) + } + _ => handle_execute(ctx, msg), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::GetSchema {} => encode_binary(&get_schema(deps)?), + QueryMsg::GetAllSubmissions {} => encode_binary(&get_all_submissions(deps.storage)?), + QueryMsg::GetSubmission { + submission_id, + wallet_address, + } => encode_binary(&get_submission(deps, submission_id, wallet_address)?), + QueryMsg::GetSubmissionIds { wallet_address } => { + encode_binary(&get_submission_ids(deps, wallet_address)?) + } + QueryMsg::GetFormStatus {} => encode_binary(&get_form_status(deps.storage, env)?), + _ => ADOContract::default().query(deps, env, msg), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + ADOContract::default().migrate(deps, CONTRACT_NAME, CONTRACT_VERSION) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result { + if msg.result.is_err() { + return Err(ContractError::Std(StdError::generic_err( + msg.result.unwrap_err(), + ))); + } + + Ok(Response::default()) +} diff --git a/contracts/data-storage/andromeda-form/src/execute.rs b/contracts/data-storage/andromeda-form/src/execute.rs new file mode 100644 index 000000000..8fd7d1055 --- /dev/null +++ b/contracts/data-storage/andromeda-form/src/execute.rs @@ -0,0 +1,436 @@ +use andromeda_data_storage::form::{ExecuteMsg, SubmissionInfo}; +use andromeda_modules::schema::{QueryMsg as SchemaQueryMsg, ValidateDataResponse}; +use andromeda_std::{ + ado_contract::ADOContract, + amp::AndrAddr, + common::{actions::call_action, context::ExecuteContext, encode_binary, Milliseconds}, + error::ContractError, +}; +use cosmwasm_std::{ensure, Env, QueryRequest, Response, Uint64, WasmQuery}; +use cw_utils::{nonpayable, Expiration}; + +use crate::{ + contract::SUBMIT_FORM_ACTION, + state::{submissions, Config, CONFIG, SCHEMA_ADO_ADDRESS, SUBMISSION_ID}, +}; + +const MAX_LIMIT: u64 = 100u64; + +pub fn handle_execute(mut ctx: ExecuteContext, msg: ExecuteMsg) -> Result { + let action_response = call_action( + &mut ctx.deps, + &ctx.info, + &ctx.env, + &ctx.amp_ctx, + msg.as_ref(), + )?; + + let res = match msg { + ExecuteMsg::SubmitForm { data } => execute_submit_form(ctx, data), + ExecuteMsg::DeleteSubmission { + submission_id, + wallet_address, + } => execute_delete_submission(ctx, submission_id, wallet_address), + ExecuteMsg::EditSubmission { + submission_id, + wallet_address, + data, + } => execute_edit_submission(ctx, submission_id, wallet_address, data), + ExecuteMsg::OpenForm {} => execute_open_form(ctx), + ExecuteMsg::CloseForm {} => execute_close_form(ctx), + _ => ADOContract::default().execute(ctx, msg), + }?; + + Ok(res + .add_submessages(action_response.messages) + .add_attributes(action_response.attributes) + .add_events(action_response.events)) +} + +pub fn execute_submit_form( + mut ctx: ExecuteContext, + data: String, +) -> Result { + nonpayable(&ctx.info)?; + let sender = ctx.info.sender; + ADOContract::default().is_permissioned( + ctx.deps.branch(), + ctx.env.clone(), + SUBMIT_FORM_ACTION, + sender.clone(), + )?; + + let config = CONFIG.load(ctx.deps.storage)?; + validate_form_is_opened(ctx.env, config.clone())?; + + let schema_ado_address = SCHEMA_ADO_ADDRESS.load(ctx.deps.storage)?; + let data_to_validate = data.clone(); + let validate_res: ValidateDataResponse = + ctx.deps + .querier + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: schema_ado_address + .get_raw_address(&ctx.deps.as_ref())? + .into_string(), + msg: encode_binary(&SchemaQueryMsg::ValidateData { + data: data_to_validate, + })?, + }))?; + + let submission_id = SUBMISSION_ID.load(ctx.deps.storage)?; + let new_id = submission_id.checked_add(Uint64::one())?; + + match validate_res { + ValidateDataResponse::Valid => { + let allow_multiple_submissions = config.allow_multiple_submissions; + match allow_multiple_submissions { + true => { + submissions().save( + ctx.deps.storage, + &(new_id.u64(), sender.clone()), + &SubmissionInfo { + submission_id: new_id.u64(), + wallet_address: sender.clone(), + data, + }, + )?; + SUBMISSION_ID.save(ctx.deps.storage, &new_id)?; + } + false => { + let submissions_by_address: Vec = submissions() + .idx + .wallet_address + .prefix(sender.clone()) + .range(ctx.deps.storage, None, None, cosmwasm_std::Order::Ascending) + .take(MAX_LIMIT as usize) + .map(|r| r.unwrap().1) + .collect(); + + if submissions_by_address.is_empty() { + submissions().save( + ctx.deps.storage, + &(new_id.u64(), sender.clone()), + &SubmissionInfo { + submission_id: new_id.u64(), + wallet_address: sender.clone(), + data, + }, + )?; + SUBMISSION_ID.save(ctx.deps.storage, &new_id)?; + } else { + return Err(ContractError::CustomError { + msg: "Multiple submissions are not allowed".to_string(), + }); + } + } + } + } + ValidateDataResponse::Invalid { msg } => return Err(ContractError::CustomError { msg }), + } + + let response = Response::new() + .add_attribute("method", "submit_form") + .add_attribute("submission_id", new_id) + .add_attribute("sender", sender.clone()); + + Ok(response) +} + +pub fn execute_delete_submission( + ctx: ExecuteContext, + submission_id: u64, + wallet_address: AndrAddr, +) -> Result { + nonpayable(&ctx.info)?; + let sender = ctx.info.sender; + ensure!( + ADOContract::default().is_owner_or_operator(ctx.deps.storage, sender.as_ref())?, + ContractError::Unauthorized {} + ); + + let address = wallet_address.get_raw_address(&ctx.deps.as_ref())?; + submissions() + .load(ctx.deps.storage, &(submission_id, address.clone())) + .map_err(|_| ContractError::CustomError { + msg: format!( + "Submission does not exist - Submission_id {:?}, Wallet_address {:?}", + submission_id, wallet_address + ), + })?; + submissions().remove(ctx.deps.storage, &(submission_id, address.clone()))?; + + let response = Response::new() + .add_attribute("method", "delete_submission") + .add_attribute("submission_id", Uint64::from(submission_id)) + .add_attribute("sender", sender); + + Ok(response) +} + +pub fn execute_edit_submission( + ctx: ExecuteContext, + submission_id: u64, + wallet_address: AndrAddr, + data: String, +) -> Result { + nonpayable(&ctx.info)?; + let sender = ctx.info.sender; + + let config = CONFIG.load(ctx.deps.storage)?; + let allow_edit_submission = config.allow_edit_submission; + ensure!( + allow_edit_submission, + ContractError::CustomError { + msg: "Edit submission is not allowed".to_string(), + } + ); + // validate if the Form is opened + validate_form_is_opened(ctx.env, config.clone())?; + + let schema_ado_address = SCHEMA_ADO_ADDRESS.load(ctx.deps.storage)?; + let data_to_validate = data.clone(); + let validate_res: ValidateDataResponse = + ctx.deps + .querier + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: schema_ado_address + .get_raw_address(&ctx.deps.as_ref())? + .into_string(), + msg: encode_binary(&SchemaQueryMsg::ValidateData { + data: data_to_validate, + })?, + }))?; + + let wallet_address = wallet_address.get_raw_address(&ctx.deps.as_ref())?; + + if submissions() + .may_load(ctx.deps.storage, &(submission_id, wallet_address.clone()))? + .is_some() + { + ensure!( + wallet_address.clone() == sender.clone(), + ContractError::Unauthorized {} + ); + + match validate_res { + ValidateDataResponse::Valid => { + submissions().save( + ctx.deps.storage, + &(submission_id, wallet_address), + &SubmissionInfo { + submission_id, + wallet_address: sender.clone(), + data: data.clone(), + }, + )?; + } + ValidateDataResponse::Invalid { msg } => { + return Err(ContractError::CustomError { msg }) + } + } + } else { + return Err(ContractError::CustomError { + msg: format!( + "Submission is not existed - Submission_id {:?}, Wallet_address {:?}", + submission_id, wallet_address + ), + }); + } + + let response = Response::new() + .add_attribute("method", "edit_submission") + .add_attribute("submission_id", Uint64::from(submission_id)) + .add_attribute("sender", sender); + + Ok(response) +} + +pub fn execute_open_form(ctx: ExecuteContext) -> Result { + nonpayable(&ctx.info)?; + let sender = ctx.info.sender; + ensure!( + ADOContract::default().is_owner_or_operator(ctx.deps.storage, sender.as_ref())?, + ContractError::Unauthorized {} + ); + + let mut config = CONFIG.load(ctx.deps.storage)?; + + let current_time = Milliseconds::from_nanos(ctx.env.block.time.nanos()); + let start_time = current_time.plus_milliseconds(Milliseconds(1)); + + let saved_start_time = config.start_time; + let saved_end_time = config.end_time; + match saved_start_time { + // If a start time is already configured: + Some(saved_start_time) => match saved_end_time { + // If both start time and end time are configured: + Some(saved_end_time) => { + // If the start time is in the future, update the start time to the current start_time value. + if saved_start_time.gt(&start_time) { + config.start_time = Some(start_time); + CONFIG.save(ctx.deps.storage, &config)?; + } + // If the form is still open (end time is in the future), return an error as the form is already open. + else if saved_end_time.gt(&start_time) { + return Err(ContractError::CustomError { + msg: format!("Already opened. Opened time {:?}", saved_start_time), + }); + } + // Otherwise, the form was closed. Update the start time to reopen the form and clear the end time. + else { + config.start_time = Some(start_time); + config.end_time = None; + CONFIG.save(ctx.deps.storage, &config)?; + } + } + + // If only the start time is configured (no end time): + None => { + // Update the start time if the saved start time is in the future. + if saved_start_time.gt(&start_time) { + config.start_time = Some(start_time); + CONFIG.save(ctx.deps.storage, &config)?; + } + // Otherwise, the form is already open, return an error. + else { + return Err(ContractError::CustomError { + msg: format!("Already opened. Opened time {:?}", saved_start_time), + }); + } + } + }, + // If no start time is configured: + None => { + // Set the start time to the current start_time value. + config.start_time = Some(start_time); + CONFIG.save(ctx.deps.storage, &config)?; + + // If an end time exists and is in the past, clear it to reopen the form. + if let Some(saved_end_time) = saved_end_time { + if start_time.gt(&saved_end_time) { + config.end_time = None; + CONFIG.save(ctx.deps.storage, &config)?; + } + } + } + } + + let response = Response::new() + .add_attribute("method", "open_form") + .add_attribute("sender", sender); + + Ok(response) +} + +pub fn execute_close_form(ctx: ExecuteContext) -> Result { + nonpayable(&ctx.info)?; + let sender = ctx.info.sender; + ensure!( + ADOContract::default().is_owner_or_operator(ctx.deps.storage, sender.as_ref())?, + ContractError::Unauthorized {} + ); + + let current_time = Milliseconds::from_nanos(ctx.env.block.time.nanos()); + let end_time = current_time.plus_milliseconds(Milliseconds(1)); + + let mut config = CONFIG.load(ctx.deps.storage)?; + let saved_start_time = config.start_time; + let saved_end_time = config.end_time; + match saved_end_time { + // If an end time is configured: + Some(saved_end_time) => match saved_start_time { + // If both start time and end time are configured: + Some(saved_start_time) => { + // If the form start time is in the future, return an error indicating the form isn't open yet. + if saved_start_time.gt(&end_time) { + return Err(ContractError::CustomError { + msg: format!("Not opened yet. Will be opened at {:?}", saved_start_time), + }); + } + // If the form is still open (end time is in the future), update the end time to the current end_time value. + else if saved_end_time.gt(&end_time) { + config.end_time = Some(end_time); + CONFIG.save(ctx.deps.storage, &config)?; + } + // Otherwise, the form has already been closed. Return an error. + else { + return Err(ContractError::CustomError { + msg: format!("Already closed. Closed at {:?}", saved_end_time), + }); + } + } + // If no start time is configured: + None => { + // Return an error indicating the form has not been opened yet. + return Err(ContractError::CustomError { + msg: "Not opened yet".to_string(), + }); + } + }, + // If no end time is configured: + None => match saved_start_time { + // If the start time exists: + Some(saved_start_time) => { + // If the start time is in the future, return an error indicating the form isn't open yet. + if saved_start_time.gt(&end_time) { + return Err(ContractError::CustomError { + msg: format!("Not opened yet. Will be opened at {:?}", saved_start_time), + }); + } + // Otherwise, set the end time to the current end_time value to close the form. + else { + config.end_time = Some(end_time); + CONFIG.save(ctx.deps.storage, &config)?; + } + } + // If no start time exists: + None => { + // Return an error indicating the form has not been opened yet. + return Err(ContractError::CustomError { + msg: "Not opened yet".to_string(), + }); + } + }, + } + + let response = Response::new() + .add_attribute("method", "close_form") + .add_attribute("sender", sender); + + Ok(response) +} + +pub fn milliseconds_from_expiration(expiration: Expiration) -> Result { + match expiration { + Expiration::AtTime(time) => Ok(Milliseconds::from_nanos(time.nanos())), + _ => Err(ContractError::CustomError { + msg: "Not supported expiration enum".to_string(), + }), + } +} + +pub fn validate_form_is_opened(env: Env, config: Config) -> Result<(), ContractError> { + let current_time = Milliseconds::from_nanos(env.block.time.nanos()); + let saved_start_time = config.start_time; + let saved_end_time = config.end_time; + match saved_start_time { + Some(saved_start_time) => { + if saved_start_time.gt(¤t_time) { + return Err(ContractError::CustomError { + msg: format!("Not opened yet. Will be opened at {:?}", saved_start_time), + }); + } + if let Some(saved_end_time) = saved_end_time { + if current_time.gt(&saved_end_time) { + return Err(ContractError::CustomError { + msg: format!("Already closed. Closed at {:?}", saved_end_time), + }); + } + } + Ok(()) + } + None => Err(ContractError::CustomError { + msg: "Not opened yet. Start time is not set".to_string(), + }), + } +} diff --git a/contracts/data-storage/andromeda-form/src/interface.rs b/contracts/data-storage/andromeda-form/src/interface.rs new file mode 100644 index 000000000..8eede40a4 --- /dev/null +++ b/contracts/data-storage/andromeda-form/src/interface.rs @@ -0,0 +1,6 @@ +use andromeda_data_storage::form::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::{ado_base::MigrateMsg, contract_interface, deploy::ADOMetadata}; + +pub const CONTRACT_ID: &str = "form"; + +contract_interface!(FormContract, CONTRACT_ID, "andromeda_form.wasm"); diff --git a/contracts/data-storage/andromeda-form/src/lib.rs b/contracts/data-storage/andromeda-form/src/lib.rs new file mode 100644 index 000000000..16c926105 --- /dev/null +++ b/contracts/data-storage/andromeda-form/src/lib.rs @@ -0,0 +1,14 @@ +pub mod contract; +pub mod execute; +pub mod query; +pub mod state; +#[cfg(test)] +pub mod testing; + +#[cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +pub mod mock; + +#[cfg(not(target_arch = "wasm32"))] +mod interface; +#[cfg(not(target_arch = "wasm32"))] +pub use crate::interface::FormContract; diff --git a/contracts/data-storage/andromeda-form/src/mock.rs b/contracts/data-storage/andromeda-form/src/mock.rs new file mode 100644 index 000000000..e9d978ee9 --- /dev/null +++ b/contracts/data-storage/andromeda-form/src/mock.rs @@ -0,0 +1,210 @@ +#![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +use crate::contract::{execute, instantiate, query}; +use andromeda_data_storage::form::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_data_storage::form::{ + FormConfig, GetAllSubmissionsResponse, GetFormStatusResponse, GetSchemaResponse, + GetSubmissionResponse, +}; +use andromeda_std::amp::AndrAddr; +use andromeda_testing::mock::MockApp; +use andromeda_testing::{ + mock_ado, + mock_contract::{ExecuteResult, MockADO, MockContract}, +}; +use cosmwasm_std::{Addr, Coin, Empty}; +use cw_multi_test::{Contract, ContractWrapper}; + +pub struct MockForm(Addr); +mock_ado!(MockForm, ExecuteMsg, QueryMsg); + +impl MockForm { + pub fn instantiate( + code_id: u64, + sender: Addr, + app: &mut MockApp, + kernel_address: String, + owner: Option, + schema_ado_address: AndrAddr, + authorized_addresses_for_submission: Option>, + form_config: FormConfig, + custom_key_for_notifications: Option, + ) -> MockForm { + let msg = mock_form_instantiate_msg( + kernel_address, + owner, + schema_ado_address, + authorized_addresses_for_submission, + form_config, + custom_key_for_notifications, + ); + let addr = app + .instantiate_contract( + code_id, + sender.clone(), + &msg, + &[], + "Form Contract", + Some(sender.to_string()), + ) + .unwrap(); + MockForm(Addr::unchecked(addr)) + } + + pub fn execute_submit_form( + &self, + app: &mut MockApp, + sender: Addr, + funds: Option, + data: String, + ) -> ExecuteResult { + let msg = ExecuteMsg::SubmitForm { data }; + + // Conditionally build the funds vector + let funds_vec = match funds { + Some(funds) => vec![funds], + None => vec![], + }; + + // Call the method once + app.execute_contract(sender, self.addr().clone(), &msg, &funds_vec) + } + + pub fn execute_delete_submission( + &self, + app: &mut MockApp, + sender: Addr, + funds: Option, + submission_id: u64, + wallet_address: AndrAddr, + ) -> ExecuteResult { + let msg = ExecuteMsg::DeleteSubmission { + submission_id, + wallet_address, + }; + + // Conditionally build the funds vector + let funds_vec = match funds { + Some(funds) => vec![funds], + None => vec![], + }; + + // Call the method once + app.execute_contract(sender, self.addr().clone(), &msg, &funds_vec) + } + + pub fn execute_edit_submission( + &self, + app: &mut MockApp, + sender: Addr, + funds: Option, + submission_id: u64, + wallet_address: AndrAddr, + data: String, + ) -> ExecuteResult { + let msg = ExecuteMsg::EditSubmission { + submission_id, + wallet_address, + data, + }; + + // Conditionally build the funds vector + let funds_vec = match funds { + Some(funds) => vec![funds], + None => vec![], + }; + + // Call the method once + app.execute_contract(sender, self.addr().clone(), &msg, &funds_vec) + } + + pub fn execute_open_form( + &self, + app: &mut MockApp, + sender: Addr, + funds: Option, + ) -> ExecuteResult { + let msg = ExecuteMsg::OpenForm {}; + + // Conditionally build the funds vector + let funds_vec = match funds { + Some(funds) => vec![funds], + None => vec![], + }; + + // Call the method once + app.execute_contract(sender, self.addr().clone(), &msg, &funds_vec) + } + + pub fn execute_close_form( + &self, + app: &mut MockApp, + sender: Addr, + funds: Option, + ) -> ExecuteResult { + let msg = ExecuteMsg::CloseForm {}; + + // Conditionally build the funds vector + let funds_vec = match funds { + Some(funds) => vec![funds], + None => vec![], + }; + + // Call the method once + app.execute_contract(sender, self.addr().clone(), &msg, &funds_vec) + } + + pub fn query_schema(&self, app: &mut MockApp) -> GetSchemaResponse { + let msg = QueryMsg::GetSchema {}; + let res: GetSchemaResponse = self.query(app, msg); + res + } + + pub fn query_form_status(&self, app: &mut MockApp) -> GetFormStatusResponse { + let msg = QueryMsg::GetFormStatus {}; + let res: GetFormStatusResponse = self.query(app, msg); + res + } + + pub fn query_all_submissions(&self, app: &mut MockApp) -> GetAllSubmissionsResponse { + let msg = QueryMsg::GetAllSubmissions {}; + let res: GetAllSubmissionsResponse = self.query(app, msg); + res + } + + pub fn query_submission( + &self, + app: &mut MockApp, + submission_id: u64, + wallet_address: AndrAddr, + ) -> GetSubmissionResponse { + let msg = QueryMsg::GetSubmission { + submission_id, + wallet_address, + }; + let res: GetSubmissionResponse = self.query(app, msg); + res + } +} + +pub fn mock_andromeda_form() -> Box> { + let contract = ContractWrapper::new_with_empty(execute, instantiate, query); + Box::new(contract) +} + +pub fn mock_form_instantiate_msg( + kernel_address: impl Into, + owner: Option, + schema_ado_address: AndrAddr, + authorized_addresses_for_submission: Option>, + form_config: FormConfig, + custom_key_for_notifications: Option, +) -> InstantiateMsg { + InstantiateMsg { + kernel_address: kernel_address.into(), + owner, + schema_ado_address, + authorized_addresses_for_submission, + form_config, + custom_key_for_notifications, + } +} diff --git a/contracts/data-storage/andromeda-form/src/query.rs b/contracts/data-storage/andromeda-form/src/query.rs new file mode 100644 index 000000000..dac0bc5df --- /dev/null +++ b/contracts/data-storage/andromeda-form/src/query.rs @@ -0,0 +1,74 @@ +use andromeda_data_storage::form::{ + GetAllSubmissionsResponse, GetFormStatusResponse, GetSchemaResponse, GetSubmissionIdsResponse, + GetSubmissionResponse, SubmissionInfo, +}; +use andromeda_modules::schema::{GetSchemaResponse as SchemaResponse, QueryMsg as SchemaQueryMsg}; +use andromeda_std::{amp::AndrAddr, common::encode_binary, error::ContractError}; +use cosmwasm_std::{Deps, Env, StdResult, Storage}; +use cosmwasm_std::{QueryRequest, WasmQuery}; + +use crate::execute::validate_form_is_opened; +use crate::state::{submissions, CONFIG, SCHEMA_ADO_ADDRESS}; + +pub fn get_schema(deps: Deps) -> Result { + let schema_ado_address = SCHEMA_ADO_ADDRESS.load(deps.storage)?; + let res: SchemaResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: schema_ado_address.get_raw_address(&deps)?.into_string(), + msg: encode_binary(&SchemaQueryMsg::GetSchema {})?, + }))?; + let schema = res.schema; + Ok(GetSchemaResponse { schema }) +} + +pub fn get_form_status( + storage: &dyn Storage, + env: Env, +) -> Result { + let config = CONFIG.load(storage)?; + // validate if the Form is opened + let res_validation = validate_form_is_opened(env, config); + if res_validation.is_ok() { + Ok(GetFormStatusResponse::Opened) + } else { + Ok(GetFormStatusResponse::Closed) + } +} + +pub fn get_all_submissions( + storage: &dyn Storage, +) -> Result { + let all_submissions: Vec = submissions() + .idx + .submission_id + .range(storage, None, None, cosmwasm_std::Order::Ascending) + .map(|result| result.map(|(_, submission)| submission)) + .collect::>()?; + Ok(GetAllSubmissionsResponse { all_submissions }) +} + +pub fn get_submission( + deps: Deps, + submission_id: u64, + wallet_address: AndrAddr, +) -> Result { + let wallet_address = wallet_address.get_raw_address(&deps)?; + let submission = + submissions().may_load(deps.storage, &(submission_id, wallet_address.clone()))?; + Ok(GetSubmissionResponse { submission }) +} + +pub fn get_submission_ids( + deps: Deps, + wallet_address: AndrAddr, +) -> Result { + let submissions_map = submissions(); + let submission_ids: Vec = submissions_map + .idx + .wallet_address + .prefix(wallet_address.get_raw_address(&deps)?) + .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .map(|result| result.map(|(_, submission)| submission.submission_id)) + .collect::>()?; + + Ok(GetSubmissionIdsResponse { submission_ids }) +} diff --git a/contracts/data-storage/andromeda-form/src/state.rs b/contracts/data-storage/andromeda-form/src/state.rs new file mode 100644 index 000000000..bc661c871 --- /dev/null +++ b/contracts/data-storage/andromeda-form/src/state.rs @@ -0,0 +1,52 @@ +use andromeda_data_storage::form::SubmissionInfo; +use andromeda_std::{amp::AndrAddr, common::MillisecondsExpiration}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint64}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex}; + +pub const SCHEMA_ADO_ADDRESS: Item = Item::new("schema_ado_address"); +pub const CONFIG: Item = Item::new("config"); +pub const SUBMISSION_ID: Item = Item::new("submission_id"); + +#[cw_serde] +pub struct Config { + pub start_time: Option, + pub end_time: Option, + pub allow_multiple_submissions: bool, + pub allow_edit_submission: bool, +} + +pub struct SubmissionIndexes<'a> { + /// PK: submission_id + wallet_address + /// Secondary key: submission_id + pub submission_id: MultiIndex<'a, u64, SubmissionInfo, (u64, Addr)>, + + /// PK: submission_id + wallet_address + /// Secondary key: wallet_address + pub wallet_address: MultiIndex<'a, Addr, SubmissionInfo, (u64, Addr)>, +} + +impl<'a> IndexList for SubmissionIndexes<'a> { + fn get_indexes( + &'_ self, + ) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.submission_id, &self.wallet_address]; + Box::new(v.into_iter()) + } +} + +pub fn submissions<'a>() -> IndexedMap<'a, &'a (u64, Addr), SubmissionInfo, SubmissionIndexes<'a>> { + let indexes = SubmissionIndexes { + submission_id: MultiIndex::new( + |_pk: &[u8], r| r.submission_id, + "submission", + "submission_id_index", + ), + wallet_address: MultiIndex::new( + |_pk: &[u8], r| r.wallet_address.clone(), + "submission", + "wallet_address_index", + ), + }; + IndexedMap::new("submission", indexes) +} diff --git a/contracts/data-storage/andromeda-form/src/testing/mock.rs b/contracts/data-storage/andromeda-form/src/testing/mock.rs new file mode 100644 index 000000000..b74e4634b --- /dev/null +++ b/contracts/data-storage/andromeda-form/src/testing/mock.rs @@ -0,0 +1,198 @@ +use andromeda_data_storage::form::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_data_storage::form::{ + FormConfig, GetAllSubmissionsResponse, GetFormStatusResponse, GetSchemaResponse, + GetSubmissionIdsResponse, GetSubmissionResponse, +}; +use andromeda_std::{ + amp::AndrAddr, error::ContractError, testing::mock_querier::MOCK_KERNEL_CONTRACT, +}; +use cosmwasm_std::{ + from_json, + testing::{mock_env, mock_info, MockApi, MockStorage}, + Deps, DepsMut, MessageInfo, OwnedDeps, Response, Timestamp, +}; + +use crate::contract::{execute, instantiate, query}; +use crate::testing::mock_querier::{mock_dependencies_custom, WasmMockQuerier}; + +pub type MockDeps = OwnedDeps; + +pub fn valid_initialization( + schema_ado_address: AndrAddr, + authorized_addresses_for_submission: Option>, + form_config: FormConfig, + custom_key_for_notifications: Option, + timestamp_nanos: u64, +) -> (MockDeps, MessageInfo, Response) { + let mut deps = mock_dependencies_custom(&[]); + let info = mock_info("creator", &[]); + let msg = InstantiateMsg { + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + owner: None, + schema_ado_address, + authorized_addresses_for_submission, + form_config, + custom_key_for_notifications, + }; + let mut env = mock_env(); + env.block.time = Timestamp::from_nanos(timestamp_nanos); + let response = instantiate(deps.as_mut(), env, info.clone(), msg).unwrap(); + + (deps, info.clone(), response) +} + +pub fn invalid_initialization( + schema_ado_address: AndrAddr, + authorized_addresses_for_submission: Option>, + form_config: FormConfig, + custom_key_for_notifications: Option, + timestamp_nanos: u64, +) -> (MockDeps, MessageInfo, ContractError) { + let mut deps = mock_dependencies_custom(&[]); + let info = mock_info("creator", &[]); + let msg = InstantiateMsg { + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + owner: None, + schema_ado_address, + authorized_addresses_for_submission, + form_config, + custom_key_for_notifications, + }; + let mut env = mock_env(); + env.block.time = Timestamp::from_nanos(timestamp_nanos); + let err = instantiate(deps.as_mut(), env, info.clone(), msg).unwrap_err(); + + (deps, info.clone(), err) +} + +pub fn submit_form( + deps: DepsMut<'_>, + sender: &str, + data: String, + timestamp_nanos: u64, +) -> Result { + let msg = ExecuteMsg::SubmitForm { data }; + let info = mock_info(sender, &[]); + let mut env = mock_env(); + env.block.time = Timestamp::from_nanos(timestamp_nanos); + execute(deps, env, info, msg) +} + +pub fn delete_submission( + deps: DepsMut<'_>, + sender: &str, + submission_id: u64, + wallet_address: AndrAddr, +) -> Result { + let msg = ExecuteMsg::DeleteSubmission { + submission_id, + wallet_address, + }; + let info = mock_info(sender, &[]); + let env = mock_env(); + execute(deps, env, info, msg) +} + +pub fn edit_submission( + deps: DepsMut<'_>, + sender: &str, + submission_id: u64, + wallet_address: AndrAddr, + data: String, +) -> Result { + let msg = ExecuteMsg::EditSubmission { + submission_id, + wallet_address, + data, + }; + let info = mock_info(sender, &[]); + let env = mock_env(); + execute(deps, env, info, msg) +} + +pub fn open_form( + deps: DepsMut<'_>, + sender: &str, + timestamp_nanos: u64, +) -> Result { + let msg = ExecuteMsg::OpenForm {}; + let info = mock_info(sender, &[]); + let mut env = mock_env(); + env.block.time = Timestamp::from_nanos(timestamp_nanos); + execute(deps, env, info, msg) +} + +pub fn close_form( + deps: DepsMut<'_>, + sender: &str, + timestamp_nanos: u64, +) -> Result { + let msg = ExecuteMsg::CloseForm {}; + let info = mock_info(sender, &[]); + let mut env = mock_env(); + env.block.time = Timestamp::from_nanos(timestamp_nanos); + execute(deps, env, info, msg) +} + +pub fn query_schema(deps: Deps) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetSchema {}); + match res { + Ok(res) => Ok(from_json(res)?), + Err(err) => Err(err), + } +} + +pub fn query_form_status( + deps: Deps, + timestamp_nanos: u64, +) -> Result { + let mut env = mock_env(); + env.block.time = Timestamp::from_nanos(timestamp_nanos); + let res = query(deps, env, QueryMsg::GetFormStatus {}); + match res { + Ok(res) => Ok(from_json(res)?), + Err(err) => Err(err), + } +} + +pub fn query_all_submissions(deps: Deps) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetAllSubmissions {}); + match res { + Ok(res) => Ok(from_json(res)?), + Err(err) => Err(err), + } +} + +pub fn query_submission( + deps: Deps, + submission_id: u64, + wallet_address: AndrAddr, +) -> Result { + let res = query( + deps, + mock_env(), + QueryMsg::GetSubmission { + submission_id, + wallet_address, + }, + ); + match res { + Ok(res) => Ok(from_json(res)?), + Err(err) => Err(err), + } +} + +pub fn query_submission_ids( + deps: Deps, + wallet_address: AndrAddr, +) -> Result { + let res = query( + deps, + mock_env(), + QueryMsg::GetSubmissionIds { wallet_address }, + ); + match res { + Ok(res) => Ok(from_json(res)?), + Err(err) => Err(err), + } +} diff --git a/contracts/data-storage/andromeda-form/src/testing/mock_querier.rs b/contracts/data-storage/andromeda-form/src/testing/mock_querier.rs new file mode 100644 index 000000000..bb4ca00fc --- /dev/null +++ b/contracts/data-storage/andromeda-form/src/testing/mock_querier.rs @@ -0,0 +1,110 @@ +use andromeda_data_storage::form::GetSchemaResponse; +use andromeda_modules::schema::{QueryMsg as SchemaQueryMsg, ValidateDataResponse}; +use andromeda_std::testing::mock_querier::MockAndromedaQuerier; +use andromeda_std::{ + ado_base::InstantiateMsg, ado_contract::ADOContract, + testing::mock_querier::MOCK_KERNEL_CONTRACT, +}; +use cosmwasm_std::QuerierWrapper; +use cosmwasm_std::{ + from_json, + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}, + Coin, OwnedDeps, Querier, QuerierResult, QueryRequest, SystemError, SystemResult, WasmQuery, +}; +use cosmwasm_std::{to_json_binary, Binary, ContractResult}; + +pub const MOCK_SCHEMA_ADO: &str = "schema_ado"; + +/// Alternative to `cosmwasm_std::testing::mock_dependencies` that allows us to respond to custom queries. +/// +/// Automatically assigns a kernel address as MOCK_KERNEL_CONTRACT. +pub fn mock_dependencies_custom( + contract_balance: &[Coin], +) -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + let storage = MockStorage::default(); + let mut deps = OwnedDeps { + storage, + api: MockApi::default(), + querier: custom_querier, + custom_query_type: std::marker::PhantomData, + }; + ADOContract::default() + .instantiate( + &mut deps.storage, + mock_env(), + &deps.api, + &QuerierWrapper::new(&deps.querier), + mock_info("sender", &[]), + InstantiateMsg { + ado_type: "form".to_string(), + ado_version: "test".to_string(), + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + owner: None, + }, + ) + .unwrap(); + deps +} +pub struct WasmMockQuerier { + pub base: MockQuerier, +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely here + let request: QueryRequest = match from_json(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {e}"), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + match contract_addr.as_str() { + MOCK_SCHEMA_ADO => self.handle_schema_smart_query(msg), + _ => MockAndromedaQuerier::default().handle_query(&self.base, request), + } + } + _ => MockAndromedaQuerier::default().handle_query(&self.base, request), + } + } + + fn handle_schema_smart_query(&self, msg: &Binary) -> QuerierResult { + match from_json(msg).unwrap() { + SchemaQueryMsg::GetSchema {} => { + let msg_response = GetSchemaResponse { + schema: "{\"properties\":{\"age\":{\"type\":\"number\"},\"name\":{\"type\":\"string\"}},\"required\":[\"name\",\"age\"],\"type\":\"object\"}".to_string(), + }; + SystemResult::Ok(ContractResult::Ok(to_json_binary(&msg_response).unwrap())) + } + SchemaQueryMsg::ValidateData { data } => match data.as_str().starts_with("valid") { + true => { + let msg_response = ValidateDataResponse::Valid; + SystemResult::Ok(ContractResult::Ok(to_json_binary(&msg_response).unwrap())) + } + false => { + let msg_response = ValidateDataResponse::Invalid { + msg: "Invalid data against schema".to_string(), + }; + SystemResult::Ok(ContractResult::Ok(to_json_binary(&msg_response).unwrap())) + } + }, + _ => panic!("Unsupported Query"), + } + } + + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { base } + } +} diff --git a/contracts/data-storage/andromeda-form/src/testing/mod.rs b/contracts/data-storage/andromeda-form/src/testing/mod.rs new file mode 100644 index 000000000..217ceb8c1 --- /dev/null +++ b/contracts/data-storage/andromeda-form/src/testing/mod.rs @@ -0,0 +1,3 @@ +mod mock; +mod mock_querier; +mod tests; diff --git a/contracts/data-storage/andromeda-form/src/testing/tests.rs b/contracts/data-storage/andromeda-form/src/testing/tests.rs new file mode 100644 index 000000000..c760e6267 --- /dev/null +++ b/contracts/data-storage/andromeda-form/src/testing/tests.rs @@ -0,0 +1,670 @@ +use super::mock::{ + close_form, invalid_initialization, open_form, query_all_submissions, query_form_status, + query_schema, query_submission, query_submission_ids, valid_initialization, +}; +use andromeda_data_storage::form::{ + FormConfig, GetFormStatusResponse, GetSchemaResponse, SubmissionInfo, +}; +use andromeda_std::{ + amp::AndrAddr, + common::{expiration::Expiry, Milliseconds}, + error::ContractError, +}; +use cosmwasm_std::{testing::mock_env, Addr, Timestamp}; +use test_case::test_case; + +use crate::{ + state::CONFIG, + testing::mock::{delete_submission, edit_submission, submit_form}, +}; + +pub const MOCK_SCHEMA_ADO: &str = "schema_ado"; + +#[test_case( + FormConfig { + start_time: None, + end_time: None, + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 10000_u64; + "With none start and none end time" +)] +#[test_case( + FormConfig { + start_time: Some(Expiry::AtTime(Milliseconds::from_nanos(1000002000000_u64))), + end_time: None, + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 1000000000000_u64; + "With valid start time and none end time" +)] +#[test_case( + FormConfig { + start_time: None, + end_time: Some(Expiry::AtTime(Milliseconds::from_nanos(1000002000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 1000000000000_u64; + "With none start time and valid end time" +)] +#[test_case( + FormConfig { + start_time: Some(Expiry::AtTime(Milliseconds::from_nanos(1000002000000_u64))), + end_time: Some(Expiry::AtTime(Milliseconds::from_nanos(2000000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 1000000000000_u64; + "With valid start and end time" +)] +#[test_case( + FormConfig { + start_time: Some(Expiry::FromNow(Milliseconds::from_nanos(1000002000000_u64))), + end_time: None, + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 1000000000000_u64; + "With valid FromNow start time and none end time" +)] +#[test_case( + FormConfig { + start_time: None, + end_time: Some(Expiry::FromNow(Milliseconds::from_nanos(1000002000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 1000000000000_u64; + "With none start time and valid FromNow end time" +)] +#[test_case( + FormConfig { + start_time: Some(Expiry::FromNow(Milliseconds::from_nanos(1000000000000_u64))), + end_time: Some(Expiry::FromNow(Milliseconds::from_nanos(2000000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 1000000000000_u64; + "With valid FromNow start and end time" +)] +fn test_valid_instantiation(form_config: FormConfig, timestamp: u64) { + valid_initialization( + AndrAddr::from_string(MOCK_SCHEMA_ADO), + None, + form_config, + None, + timestamp, + ); +} + +#[test_case( + FormConfig { + start_time: Some(Expiry::AtTime(Milliseconds::from_nanos(900000000000_u64))), + end_time: None, + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 1000000000000_u64, + ContractError::StartTimeInThePast { current_time: 1000000_u64, current_block: 12345_u64 }; + "With invalid start and none end time" +)] +#[test_case( + FormConfig { + start_time: None, + end_time: Some(Expiry::AtTime(Milliseconds::from_nanos(900000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 1000000000000_u64, + ContractError::CustomError { + msg: format!( + "End time in the past. current_time {:?}, current_block {:?}", + 1000000_u64, 12345_u64 + ), + }; + "With none start and invalid end time" +)] +#[test_case( + FormConfig { + start_time: Some(Expiry::AtTime(Milliseconds::from_nanos(2000000000000_u64))), + end_time: Some(Expiry::AtTime(Milliseconds::from_nanos(1200000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 1000000000000_u64, + ContractError::StartTimeAfterEndTime {}; + "With invalid start and end time_1" +)] +#[test_case( + FormConfig { + start_time: Some(Expiry::FromNow(Milliseconds::from_nanos(2000000000000_u64))), + end_time: Some(Expiry::FromNow(Milliseconds::from_nanos(1200000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 1000000000000_u64, + ContractError::StartTimeAfterEndTime {}; + "With invalid start and end time_2" +)] +fn test_invalid_instantiation( + form_config: FormConfig, + timestamp: u64, + expected_err: ContractError, +) { + let (_, _, err) = invalid_initialization( + AndrAddr::from_string(MOCK_SCHEMA_ADO), + None, + form_config, + None, + timestamp, + ); + assert_eq!(expected_err, err); +} + +#[test_case( + FormConfig { + start_time: Some(Expiry::AtTime(Milliseconds::from_nanos(1000000000000_u64))), + end_time: None, + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 500000000000_u64, + 2000000000000_u64, + ContractError::CustomError { + msg: format!("Already opened. Opened time {:?}", Milliseconds::from_nanos(1000000000000_u64)), + }; + "Invalid timestamp at execution with saved start time" +)] +#[test_case( + FormConfig { + start_time: Some(Expiry::AtTime(Milliseconds::from_nanos(1000000000000_u64))), + end_time: Some(Expiry::AtTime(Milliseconds::from_nanos(3000000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 500000000000_u64, + 2000000000000_u64, + ContractError::CustomError { + msg: format!("Already opened. Opened time {:?}", Milliseconds::from_nanos(1000000000000_u64)), + }; + "Invalid timestamp at execution with saved start and end time" +)] +fn test_failed_open_form( + form_config: FormConfig, + instantiation_timestamp: u64, + execute_timestamp: u64, + expected_err: ContractError, +) { + let (mut deps, info, _) = valid_initialization( + AndrAddr::from_string(MOCK_SCHEMA_ADO), + None, + form_config, + None, + instantiation_timestamp, + ); + let err = open_form(deps.as_mut(), info.sender.as_ref(), execute_timestamp).unwrap_err(); + assert_eq!(expected_err, err); +} + +#[test_case( + FormConfig { + start_time: Some(Expiry::AtTime(Milliseconds::from_nanos(2000000000000_u64))), + end_time: Some(Expiry::AtTime(Milliseconds::from_nanos(3000000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 500000000000_u64, + 1000000000000_u64; + "Valid timestamp at execution with saved start and end time_1" +)] +#[test_case( + FormConfig { + start_time: Some(Expiry::AtTime(Milliseconds::from_nanos(2000000000000_u64))), + end_time: Some(Expiry::AtTime(Milliseconds::from_nanos(3000000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 500000000000_u64, + 4000000000000_u64; + "Valid timestamp at execution with saved start and end time_2" +)] +#[test_case( + FormConfig { + start_time: None, + end_time: None, + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 500000000000_u64, + 1000000000000_u64; + "Valid timestamp at execution with none start and end time" +)] +#[test_case( + FormConfig { + start_time: None, + end_time: Some(Expiry::AtTime(Milliseconds::from_nanos(4000000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 500000000000_u64, + 1000000000000_u64; + "Valid timestamp at execution with end time_1" +)] +#[test_case( + FormConfig { + start_time: None, + end_time: Some(Expiry::AtTime(Milliseconds::from_nanos(1000000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 500000000000_u64, + 3000000000000_u64; + "Valid timestamp at execution with end time_2" +)] +fn test_success_open_form( + form_config: FormConfig, + instantiation_timestamp: u64, + execute_timestamp: u64, +) { + let (mut deps, info, _) = valid_initialization( + AndrAddr::from_string(MOCK_SCHEMA_ADO), + None, + form_config.clone(), + None, + instantiation_timestamp, + ); + let res = open_form(deps.as_mut(), info.sender.as_ref(), execute_timestamp); + assert!(res.is_ok()); + + let config = CONFIG.load(&deps.storage).unwrap(); + let start_time = config.start_time; + let expected_saved_start_time = if start_time.is_some() { + Some(Milliseconds::from_nanos(execute_timestamp).plus_milliseconds(Milliseconds(1))) + } else { + None + }; + assert_eq!(expected_saved_start_time, start_time); + + let end_time = config.end_time; + + let saved_start_time = if let Some(start_time) = form_config.start_time { + let mut env = mock_env(); + env.block.time = Timestamp::from_nanos(instantiation_timestamp); + Some(start_time.get_time(&env.block)) + } else { + None + }; + let saved_end_time = if let Some(end_time) = form_config.end_time { + let mut env = mock_env(); + env.block.time = Timestamp::from_nanos(instantiation_timestamp); + Some(end_time.get_time(&env.block)) + } else { + None + }; + let execute_time = Milliseconds::from_nanos(execute_timestamp); + match saved_start_time { + Some(saved_start_time) => match saved_end_time { + Some(saved_end_time) => { + if saved_start_time.gt(&execute_time) { + assert_eq!(end_time, Some(saved_end_time)) + } else if saved_end_time.gt(&execute_time) { + assert_eq!(end_time, None); + } + } + None => { + if saved_start_time.gt(&execute_time) { + assert_eq!(end_time, None); + } + } + }, + None => { + if let Some(saved_end_time) = saved_end_time { + if execute_time.gt(&saved_end_time) { + assert_eq!(end_time, None); + } else { + assert_eq!(end_time, Some(saved_end_time)) + } + } + } + } +} + +#[test_case( + FormConfig { + start_time: Some(Expiry::AtTime(Milliseconds::from_nanos(2000000000000_u64))), + end_time: Some(Expiry::AtTime(Milliseconds::from_nanos(3000000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 500000000000_u64, + 1000000000000_u64, + ContractError::CustomError { + msg: format!("Not opened yet. Will be opened at {:?}", Milliseconds::from_nanos(2000000000000_u64)), + }; + "Invalid timestamp at execution with saved start and end time-1" +)] +#[test_case( + FormConfig { + start_time: Some(Expiry::AtTime(Milliseconds::from_nanos(2000000000000_u64))), + end_time: Some(Expiry::AtTime(Milliseconds::from_nanos(3000000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 500000000000_u64, + 4000000000000_u64, + ContractError::CustomError { + msg: format!("Already closed. Closed at {:?}", Milliseconds::from_nanos(3000000000000_u64)), + }; + "Invalid timestamp at execution with saved start and end time-2" +)] +#[test_case( + FormConfig { + start_time: None, + end_time: Some(Expiry::AtTime(Milliseconds::from_nanos(3000000000000_u64))), + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 500000000000_u64, + 4000000000000_u64, + ContractError::CustomError { + msg: "Not opened yet".to_string(), + }; + "Invalid timestamp at execution with none start time" +)] +#[test_case( + FormConfig { + start_time: Some(Expiry::AtTime(Milliseconds::from_nanos(2000000000000_u64))), + end_time: None, + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 500000000000_u64, + 1000000000000_u64, + ContractError::CustomError { + msg: format!("Not opened yet. Will be opened at {:?}", Milliseconds::from_nanos(2000000000000_u64)), + }; + "Invalid timestamp at execution with start time" +)] +#[test_case( + FormConfig { + start_time: None, + end_time: None, + allow_multiple_submissions: true, + allow_edit_submission: true, + }, + 500000000000_u64, + 1000000000000_u64, + ContractError::CustomError { + msg: "Not opened yet".to_string(), + }; + "Invalid timestamp at execution with none start and end time" +)] +fn test_failed_close_form( + form_config: FormConfig, + instantiation_timestamp: u64, + execute_timestamp: u64, + expected_err: ContractError, +) { + let (mut deps, info, _) = valid_initialization( + AndrAddr::from_string(MOCK_SCHEMA_ADO), + None, + form_config, + None, + instantiation_timestamp, + ); + let err = close_form(deps.as_mut(), info.sender.as_ref(), execute_timestamp).unwrap_err(); + assert_eq!(expected_err, err); +} + +#[test] +fn test_submit_form_allowed_multiple_submission() { + let form_config = FormConfig { + start_time: None, + end_time: None, + allow_multiple_submissions: true, + allow_edit_submission: true, + }; + let (mut deps, info, _) = valid_initialization( + AndrAddr::from_string(MOCK_SCHEMA_ADO), + None, + form_config, + None, + 5000000000_u64, + ); + open_form(deps.as_mut(), info.sender.as_ref(), 10000000000_u64).unwrap(); + + let form_status = query_form_status(deps.as_ref(), 20000000000_u64).unwrap(); + assert_eq!(form_status, GetFormStatusResponse::Opened); + + submit_form( + deps.as_mut(), + "user1", + "valid_data1".to_string(), + 20000000000_u64, + ) + .unwrap(); + submit_form( + deps.as_mut(), + "user1", + "valid_data2".to_string(), + 30000000000_u64, + ) + .unwrap(); + submit_form( + deps.as_mut(), + "user2", + "valid_data3".to_string(), + 40000000000_u64, + ) + .unwrap(); + submit_form( + deps.as_mut(), + "user3", + "valid_data4".to_string(), + 50000000000_u64, + ) + .unwrap(); + submit_form( + deps.as_mut(), + "user4", + "valid_data5".to_string(), + 60000000000_u64, + ) + .unwrap(); + submit_form( + deps.as_mut(), + "user4", + "valid_data6".to_string(), + 70000000000_u64, + ) + .unwrap(); + submit_form( + deps.as_mut(), + "user4", + "valid_data7".to_string(), + 80000000000_u64, + ) + .unwrap(); + let err = submit_form( + deps.as_mut(), + "user4", + "invalid_data8".to_string(), + 85000000000_u64, + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::CustomError { + msg: "Invalid data against schema".to_string(), + } + ); + + let all_submissions = query_all_submissions(deps.as_ref()) + .unwrap() + .all_submissions; + assert_eq!(all_submissions.len(), 7_usize); + + let submission_ids = query_submission_ids(deps.as_ref(), AndrAddr::from_string("user1")) + .unwrap() + .submission_ids; + assert_eq!(submission_ids.len(), 2_usize); + + let submission_ids = query_submission_ids(deps.as_ref(), AndrAddr::from_string("user5")) + .unwrap() + .submission_ids; + assert_eq!(submission_ids.len(), 0_usize); + + delete_submission( + deps.as_mut(), + info.sender.as_ref(), + 4, + AndrAddr::from_string("user3"), + ) + .unwrap(); + let all_submissions = query_all_submissions(deps.as_ref()) + .unwrap() + .all_submissions; + assert_eq!(all_submissions.len(), 6_usize); + + close_form(deps.as_mut(), info.sender.as_ref(), 90000000000_u64).unwrap(); + + let form_status = query_form_status(deps.as_ref(), 100000000000_u64).unwrap(); + assert_eq!(form_status, GetFormStatusResponse::Closed); + + let err = submit_form( + deps.as_mut(), + "user5", + "valid_data8".to_string(), + 110000000000_u64, + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::CustomError { + msg: format!( + "Already closed. Closed at {:?}", + Milliseconds::from_nanos(90000000000_u64).plus_milliseconds(Milliseconds(1)) + ) + } + ); +} + +#[test] +fn test_submit_form_disallowed_multiple_submission_disallowed_edit() { + let form_config = FormConfig { + start_time: None, + end_time: None, + allow_multiple_submissions: false, + allow_edit_submission: false, + }; + let (mut deps, info, _) = valid_initialization( + AndrAddr::from_string(MOCK_SCHEMA_ADO), + None, + form_config, + None, + 5000000000_u64, + ); + open_form(deps.as_mut(), info.sender.as_ref(), 10000000000_u64).unwrap(); + submit_form( + deps.as_mut(), + "user1", + "valid_data1".to_string(), + 20000000000_u64, + ) + .unwrap(); + let res = submit_form( + deps.as_mut(), + "user1", + "valid_data2".to_string(), + 30000000000_u64, + ) + .unwrap_err(); + assert_eq!( + res, + ContractError::CustomError { + msg: "Multiple submissions are not allowed".to_string(), + } + ); + + let res = edit_submission( + deps.as_mut(), + "user1", + 1, + AndrAddr::from_string("user1"), + "valid_data2".to_string(), + ) + .unwrap_err(); + assert_eq!( + res, + ContractError::CustomError { + msg: "Edit submission is not allowed".to_string(), + } + ); +} + +#[test] +fn test_submit_form_disallowed_multiple_submission_allowed_edit() { + let form_config = FormConfig { + start_time: None, + end_time: None, + allow_multiple_submissions: false, + allow_edit_submission: true, + }; + let (mut deps, info, _) = valid_initialization( + AndrAddr::from_string(MOCK_SCHEMA_ADO), + None, + form_config, + None, + 5000000000_u64, + ); + open_form(deps.as_mut(), info.sender.as_ref(), 10000000000_u64).unwrap(); + submit_form( + deps.as_mut(), + "user1", + "valid_data1".to_string(), + 20000000000_u64, + ) + .unwrap(); + + let res = edit_submission( + deps.as_mut(), + "user2", + 1, + AndrAddr::from_string("user1"), + "valid_data2".to_string(), + ) + .unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + + edit_submission( + deps.as_mut(), + "user1", + 1, + AndrAddr::from_string("user1"), + "valid_data2".to_string(), + ) + .unwrap(); + let submission = query_submission(deps.as_ref(), 1, AndrAddr::from_string("user1")) + .unwrap() + .submission; + assert_eq!( + submission, + Some(SubmissionInfo { + submission_id: 1, + wallet_address: Addr::unchecked("user1"), + data: "valid_data2".to_string() + }) + ); + + let schema = query_schema(deps.as_ref()).unwrap(); + assert_eq!( + schema, + GetSchemaResponse { + schema: "{\"properties\":{\"age\":{\"type\":\"number\"},\"name\":{\"type\":\"string\"}},\"required\":[\"name\",\"age\"],\"type\":\"object\"}".to_string(), + } + ); +} diff --git a/contracts/modules/andromeda-schema/.cargo/config b/contracts/modules/andromeda-schema/.cargo/config new file mode 100644 index 000000000..624255c74 --- /dev/null +++ b/contracts/modules/andromeda-schema/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" \ No newline at end of file diff --git a/contracts/modules/andromeda-schema/Cargo.toml b/contracts/modules/andromeda-schema/Cargo.toml new file mode 100644 index 000000000..cee6c91a8 --- /dev/null +++ b/contracts/modules/andromeda-schema/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "andromeda-schema" +version = "0.1.0-beta" +edition = "2021" +rust-version = "1.75.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] +testing = ["cw-multi-test", "andromeda-testing"] + + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw-json = { git = "https://github.com/SlayerAnsh/cw-json.git" } +serde_json = { workspace = true } +test-case = { workspace = true } + +andromeda-std = { workspace = true } +andromeda-modules = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +cw-orch = { workspace = true } +cw-multi-test = { workspace = true, optional = true } +andromeda-testing = { workspace = true, optional = true } + +[dev-dependencies] +andromeda-app = { workspace = true } diff --git a/contracts/modules/andromeda-schema/examples/schema.rs b/contracts/modules/andromeda-schema/examples/schema.rs new file mode 100644 index 000000000..218073863 --- /dev/null +++ b/contracts/modules/andromeda-schema/examples/schema.rs @@ -0,0 +1,10 @@ +use andromeda_modules::schema::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::write_api; +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + + } +} diff --git a/contracts/modules/andromeda-schema/src/contract.rs b/contracts/modules/andromeda-schema/src/contract.rs new file mode 100644 index 000000000..634f3fd3b --- /dev/null +++ b/contracts/modules/andromeda-schema/src/contract.rs @@ -0,0 +1,100 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::{ + entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError, +}; + +use andromeda_modules::schema::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +use andromeda_std::{ + ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, + ado_contract::ADOContract, + common::{context::ExecuteContext, encode_binary}, + error::ContractError, +}; +use cw_json::JSON; +use serde_json::{from_str, Value}; + +use crate::{ + execute::handle_execute, + query::{get_schema, validate_data}, + state::SCHEMA, +}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:andromeda-schema"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let resp = ADOContract::default().instantiate( + deps.storage, + env, + deps.api, + &deps.querier, + info, + BaseInstantiateMsg { + ado_type: CONTRACT_NAME.to_string(), + ado_version: CONTRACT_VERSION.to_string(), + kernel_address: msg.kernel_address, + owner: msg.owner, + }, + )?; + + let schema_json_string = msg.schema_json_string; + + let schema_json_value: Value = + from_str(schema_json_string.as_str()).map_err(|_| ContractError::CustomError { + msg: "Invalid JSON Schema".to_string(), + })?; + let schema_json = JSON::try_from(schema_json_value.to_string().as_str()).unwrap(); + + SCHEMA.save(deps.storage, &schema_json)?; + + Ok(resp) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let ctx = ExecuteContext::new(deps, info, env); + match msg { + ExecuteMsg::AMPReceive(pkt) => { + ADOContract::default().execute_amp_receive(ctx, pkt, handle_execute) + } + _ => handle_execute(ctx, msg), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::ValidateData { data } => encode_binary(&validate_data(deps.storage, data)?), + QueryMsg::GetSchema {} => encode_binary(&get_schema(deps.storage)?), + _ => ADOContract::default().query(deps, env, msg), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + ADOContract::default().migrate(deps, CONTRACT_NAME, CONTRACT_VERSION) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result { + if msg.result.is_err() { + return Err(ContractError::Std(StdError::generic_err( + msg.result.unwrap_err(), + ))); + } + + Ok(Response::default()) +} diff --git a/contracts/modules/andromeda-schema/src/execute.rs b/contracts/modules/andromeda-schema/src/execute.rs new file mode 100644 index 000000000..ee1361a2d --- /dev/null +++ b/contracts/modules/andromeda-schema/src/execute.rs @@ -0,0 +1,59 @@ +use andromeda_modules::schema::ExecuteMsg; +use andromeda_std::{ + ado_contract::ADOContract, + common::{actions::call_action, context::ExecuteContext}, + error::ContractError, +}; +use cosmwasm_std::{ensure, Response}; +use cw_json::JSON; +use serde_json::{from_str, Value}; + +use crate::state::SCHEMA; + +pub fn handle_execute(mut ctx: ExecuteContext, msg: ExecuteMsg) -> Result { + let action_response = call_action( + &mut ctx.deps, + &ctx.info, + &ctx.env, + &ctx.amp_ctx, + msg.as_ref(), + )?; + + let res = match msg { + ExecuteMsg::UpdateSchema { + new_schema_json_string, + } => execute_update_schema(ctx, new_schema_json_string), + _ => ADOContract::default().execute(ctx, msg), + }?; + + Ok(res + .add_submessages(action_response.messages) + .add_attributes(action_response.attributes) + .add_events(action_response.events)) +} + +fn execute_update_schema( + ctx: ExecuteContext, + new_schema_json: String, +) -> Result { + let sender: cosmwasm_std::Addr = ctx.info.sender; + + ensure!( + ADOContract::default().is_owner_or_operator(ctx.deps.storage, sender.as_ref())?, + ContractError::Unauthorized {} + ); + + let new_schema_json_value: Value = + from_str(new_schema_json.as_str()).map_err(|_| ContractError::CustomError { + msg: "Invalid JSON Schema".to_string(), + })?; + let new_schema_json = JSON::try_from(new_schema_json_value.to_string().as_str()).unwrap(); + + SCHEMA.save(ctx.deps.storage, &new_schema_json)?; + + let response = Response::new() + .add_attribute("method", "update_schema") + .add_attribute("sender", sender); + + Ok(response) +} diff --git a/contracts/modules/andromeda-schema/src/interface.rs b/contracts/modules/andromeda-schema/src/interface.rs new file mode 100644 index 000000000..e64c35f36 --- /dev/null +++ b/contracts/modules/andromeda-schema/src/interface.rs @@ -0,0 +1,6 @@ +use andromeda_modules::schema::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::{ado_base::MigrateMsg, contract_interface, deploy::ADOMetadata}; + +pub const CONTRACT_ID: &str = "schema"; + +contract_interface!(SchemaContract, CONTRACT_ID, "andromeda_schema.wasm"); diff --git a/contracts/modules/andromeda-schema/src/lib.rs b/contracts/modules/andromeda-schema/src/lib.rs new file mode 100644 index 000000000..bc1546615 --- /dev/null +++ b/contracts/modules/andromeda-schema/src/lib.rs @@ -0,0 +1,14 @@ +pub mod contract; +pub mod execute; +pub mod query; +pub mod state; +#[cfg(test)] +pub mod testing; + +#[cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +pub mod mock; + +#[cfg(not(target_arch = "wasm32"))] +mod interface; +#[cfg(not(target_arch = "wasm32"))] +pub use crate::interface::SchemaContract; diff --git a/contracts/modules/andromeda-schema/src/mock.rs b/contracts/modules/andromeda-schema/src/mock.rs new file mode 100644 index 000000000..39db6cffa --- /dev/null +++ b/contracts/modules/andromeda-schema/src/mock.rs @@ -0,0 +1,81 @@ +#![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +use crate::contract::{execute, instantiate, query}; +use andromeda_modules::schema::{ExecuteMsg, InstantiateMsg, QueryMsg, ValidateDataResponse}; +use andromeda_testing::mock::MockApp; +use andromeda_testing::{ + mock_ado, + mock_contract::{ExecuteResult, MockADO, MockContract}, +}; +use cosmwasm_std::{Addr, Coin, Empty}; +use cw_multi_test::{Contract, ContractWrapper}; + +pub struct MockSchema(Addr); +mock_ado!(MockSchema, ExecuteMsg, QueryMsg); + +impl MockSchema { + pub fn instantiate( + code_id: u64, + sender: Addr, + app: &mut MockApp, + kernel_address: String, + owner: Option, + schema_json_string: String, + ) -> MockSchema { + let msg = mock_schema_instantiate_msg(kernel_address, owner, schema_json_string); + let addr = app + .instantiate_contract( + code_id, + sender.clone(), + &msg, + &[], + "Schema Contract", + Some(sender.to_string()), + ) + .unwrap(); + MockSchema(Addr::unchecked(addr)) + } + + pub fn execute_update_schema( + &self, + app: &mut MockApp, + sender: Addr, + funds: Option, + new_schema_json_string: String, + ) -> ExecuteResult { + let msg = ExecuteMsg::UpdateSchema { + new_schema_json_string, + }; + + // Conditionally build the funds vector + let funds_vec = match funds { + Some(funds) => vec![funds], + None => vec![], + }; + + // Call the method once + app.execute_contract(sender, self.addr().clone(), &msg, &funds_vec) + } + + pub fn query_validate_data(&self, app: &mut MockApp, data: String) -> ValidateDataResponse { + let msg = QueryMsg::ValidateData { data }; + let res: ValidateDataResponse = self.query(app, msg); + res + } +} + +pub fn mock_andromeda_schema() -> Box> { + let contract = ContractWrapper::new_with_empty(execute, instantiate, query); + Box::new(contract) +} + +pub fn mock_schema_instantiate_msg( + kernel_address: impl Into, + owner: Option, + schema_json_string: String, +) -> InstantiateMsg { + InstantiateMsg { + kernel_address: kernel_address.into(), + owner, + schema_json_string, + } +} diff --git a/contracts/modules/andromeda-schema/src/query.rs b/contracts/modules/andromeda-schema/src/query.rs new file mode 100644 index 000000000..aa92e1b5c --- /dev/null +++ b/contracts/modules/andromeda-schema/src/query.rs @@ -0,0 +1,226 @@ +use andromeda_modules::schema::{GetSchemaResponse, ValidateDataResponse}; +use andromeda_std::error::ContractError; +use cosmwasm_std::Storage; +use serde_json::{from_str, json, Value}; + +use crate::state::SCHEMA; + +pub fn get_schema(storage: &dyn Storage) -> Result { + let schema = SCHEMA.load(storage)?.to_string(); + Ok(GetSchemaResponse { schema }) +} + +pub fn validate_data( + storage: &dyn Storage, + data: String, +) -> Result { + // Load the schema from storage + let schema: Value = json!(SCHEMA.load(storage)?); + let data_instance: Value = from_str(&data).map_err(|e| ContractError::CustomError { + msg: format!("Invalid data JSON: {}", e), + })?; + + // Perform basic validation for types: string, array, and object + if basic_type_matches(&schema, &data_instance) { + Ok(ValidateDataResponse::Valid) + } else { + Ok(ValidateDataResponse::Invalid { + msg: "Data structure does not match the basic schema types.".to_string(), + }) + } +} + +fn basic_type_matches(schema: &Value, data: &Value) -> bool { + match schema.get("type") { + Some(Value::String(schema_type)) => match schema_type.as_str() { + "string" => data.is_string(), + "number" => data.is_number(), + "boolean" => data.is_boolean(), + "array" => { + if let Some(items_schema) = schema.get("items") { + if let Value::Array(data_array) = data { + data_array + .iter() + .all(|item| basic_type_matches(items_schema, item)) + } else { + false + } + } else { + false + } + } + "object" => { + if let Some(Value::Object(schema_props)) = schema.get("properties") { + if let Value::Object(data_obj) = data { + // Check for required properties + let required_fields = schema.get("required").and_then(|r| r.as_array()); + if let Some(required_fields) = required_fields { + if !required_fields.iter().all(|field| { + field.as_str().map_or(false, |f| data_obj.contains_key(f)) + }) { + return false; + } + } + // Check each property + schema_props.iter().all(|(key, prop_schema)| { + if let Some(data_value) = data_obj.get(key) { + basic_type_matches(prop_schema, data_value) + } else { + true // Property not present, acceptable if not required + } + }) + } else { + false + } + } else { + false + } + } + _ => false, // Unsupported type + }, + _ => false, // Type not specified in schema + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_basic_type_matches_string() { + let schema = json!({ "type": "string" }); + let data = json!("Hello World"); + assert!(basic_type_matches(&schema, &data)); + + let data = json!(123); + assert!(!basic_type_matches(&schema, &data)); + } + + #[test] + fn test_basic_type_matches_number() { + let schema = json!({ "type": "number" }); + let data = json!(42); + assert!(basic_type_matches(&schema, &data)); + + let data = json!("42"); + assert!(!basic_type_matches(&schema, &data)); + } + + #[test] + fn test_basic_type_matches_boolean() { + let schema = json!({ "type": "boolean" }); + let data = json!(true); + assert!(basic_type_matches(&schema, &data)); + + let data = json!("true"); + assert!(!basic_type_matches(&schema, &data)); + } + + #[test] + fn test_basic_type_matches_array_of_strings() { + let schema = json!({ + "type": "array", + "items": { "type": "string" } + }); + let data = json!(["apple", "banana", "cherry"]); + assert!(basic_type_matches(&schema, &data)); + + let data = json!(["apple", 123, "cherry"]); + assert!(!basic_type_matches(&schema, &data)); + } + + #[test] + fn test_basic_type_matches_array_of_objects() { + let schema = json!({ + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "number" }, + "name": { "type": "string" } + }, + "required": ["id", "name"] + } + }); + let data = json!([ + { "id": 1, "name": "Alice" }, + { "id": 2, "name": "Bob" } + ]); + assert!(basic_type_matches(&schema, &data)); + + let data = json!([ + { "id": "one", "name": "Alice" }, + { "id": 2, "name": "Bob" } + ]); + assert!(!basic_type_matches(&schema, &data)); + } + + #[test] + fn test_basic_type_matches_object() { + let schema = json!({ + "type": "object", + "properties": { + "title": { "type": "string" }, + "count": { "type": "number" } + }, + "required": ["title", "count"] + }); + let data = json!({ "title": "Introduction", "count": 10 }); + assert!(basic_type_matches(&schema, &data)); + + let data = json!({ "title": "Introduction" }); + assert!(!basic_type_matches(&schema, &data)); + + let data = json!({ "title": "Introduction", "count": "ten" }); + assert!(!basic_type_matches(&schema, &data)); + } + + #[test] + fn test_basic_type_matches_nested_object() { + let schema = json!({ + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "number" } + }, + "required": ["name", "age"] + }, + "roles": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["user", "roles"] + }); + let data = json!({ + "user": { "name": "Charlie", "age": 25 }, + "roles": ["admin", "editor"] + }); + assert!(basic_type_matches(&schema, &data)); + + // Missing required property "age" + let data = json!({ + "user": { "name": "Charlie" }, + "roles": ["admin", "editor"] + }); + assert!(!basic_type_matches(&schema, &data)); + + // Incorrect type for "age" + let data = json!({ + "user": { "name": "Charlie", "age": "twenty-five" }, + "roles": ["admin", "editor"] + }); + assert!(!basic_type_matches(&schema, &data)); + + // Incorrect type in "roles" array + let data = json!({ + "user": { "name": "Charlie", "age": 25 }, + "roles": ["admin", 123] + }); + assert!(!basic_type_matches(&schema, &data)); + } +} diff --git a/contracts/modules/andromeda-schema/src/state.rs b/contracts/modules/andromeda-schema/src/state.rs new file mode 100644 index 000000000..93a48ec34 --- /dev/null +++ b/contracts/modules/andromeda-schema/src/state.rs @@ -0,0 +1,4 @@ +use cw_json::JSON; +use cw_storage_plus::Item; + +pub const SCHEMA: Item = Item::new("schema"); diff --git a/contracts/modules/andromeda-schema/src/testing/mock.rs b/contracts/modules/andromeda-schema/src/testing/mock.rs new file mode 100644 index 000000000..0d6bfaf6a --- /dev/null +++ b/contracts/modules/andromeda-schema/src/testing/mock.rs @@ -0,0 +1,49 @@ +use andromeda_modules::schema::{ + GetSchemaResponse, InstantiateMsg, QueryMsg, ValidateDataResponse, +}; +use andromeda_std::{ + error::ContractError, + testing::mock_querier::{mock_dependencies_custom, WasmMockQuerier, MOCK_KERNEL_CONTRACT}, +}; +use cosmwasm_std::{ + from_json, + testing::{mock_env, mock_info, MockApi, MockStorage}, + Deps, MessageInfo, OwnedDeps, +}; + +use crate::contract::{instantiate, query}; + +pub type MockDeps = OwnedDeps; + +pub fn proper_initialization(schema_json_string: String) -> (MockDeps, MessageInfo) { + let mut deps = mock_dependencies_custom(&[]); + let info = mock_info("creator", &[]); + let msg = InstantiateMsg { + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + owner: None, + schema_json_string, + }; + let env = mock_env(); + let res = instantiate(deps.as_mut(), env, info.clone(), msg).unwrap(); + assert_eq!(0, res.messages.len()); + (deps, info) +} + +pub fn query_validate_data( + deps: Deps, + data: String, +) -> Result { + let res = query(deps, mock_env(), QueryMsg::ValidateData { data }); + match res { + Ok(res) => Ok(from_json(res).unwrap()), + Err(err) => Err(err), + } +} + +pub fn query_schema(deps: Deps) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetSchema {}); + match res { + Ok(res) => Ok(from_json(res).unwrap()), + Err(err) => Err(err), + } +} diff --git a/contracts/modules/andromeda-schema/src/testing/mod.rs b/contracts/modules/andromeda-schema/src/testing/mod.rs new file mode 100644 index 000000000..3bfda2893 --- /dev/null +++ b/contracts/modules/andromeda-schema/src/testing/mod.rs @@ -0,0 +1,2 @@ +mod mock; +mod tests; diff --git a/contracts/modules/andromeda-schema/src/testing/tests.rs b/contracts/modules/andromeda-schema/src/testing/tests.rs new file mode 100644 index 000000000..356c0856b --- /dev/null +++ b/contracts/modules/andromeda-schema/src/testing/tests.rs @@ -0,0 +1,220 @@ +use super::mock::{proper_initialization, query_schema, query_validate_data}; +use andromeda_modules::schema::ValidateDataResponse; +use test_case::test_case; + +// JSON schema definitions for each type and nested structures +pub const SCHEMA_STRING_TYPE: &str = r#" +{ + "type": "string" +}"#; + +pub const SCHEMA_NUMBER_TYPE: &str = r#" +{ + "type": "number" +}"#; + +pub const SCHEMA_BOOLEAN_TYPE: &str = r#" +{ + "type": "boolean" +}"#; + +pub const SCHEMA_ARRAY_OF_STRINGS: &str = r#" +{ + "type": "array", + "items": { "type": "string" } +}"#; + +pub const SCHEMA_ARRAY_OF_OBJECTS: &str = r#" +{ + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "number" }, + "name": { "type": "string" } + }, + "required": ["id", "name"] + } +}"#; + +pub const SCHEMA_SIMPLE_OBJECT: &str = r#" +{ + "type": "object", + "properties": { + "title": { "type": "string" }, + "count": { "type": "number" } + }, + "required": ["title", "count"] +}"#; + +pub const SCHEMA_NESTED_OBJECT: &str = r#" +{ + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "number" } + }, + "required": ["name", "age"] + }, + "roles": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["user", "roles"] +}"#; + +#[test_case( + SCHEMA_STRING_TYPE, + r#""Hello World""#, + ValidateDataResponse::Valid ; + "valid string data" +)] +#[test_case( + SCHEMA_STRING_TYPE, + r#"123"#, + ValidateDataResponse::Invalid { msg: "Data structure does not match the basic schema types.".to_string() } ; + "invalid non-string for string schema" +)] +#[test_case( + SCHEMA_NUMBER_TYPE, + r#"42"#, + ValidateDataResponse::Valid ; + "valid number data" +)] +#[test_case( + SCHEMA_NUMBER_TYPE, + r#""forty-two""#, + ValidateDataResponse::Invalid { msg: "Data structure does not match the basic schema types.".to_string() } ; + "invalid non-number for number schema" +)] +#[test_case( + SCHEMA_BOOLEAN_TYPE, + r#"true"#, + ValidateDataResponse::Valid ; + "valid boolean data" +)] +#[test_case( + SCHEMA_BOOLEAN_TYPE, + r#""true""#, + ValidateDataResponse::Invalid { msg: "Data structure does not match the basic schema types.".to_string() } ; + "invalid non-boolean for boolean schema" +)] +#[test_case( + SCHEMA_ARRAY_OF_STRINGS, + r#"["apple", "banana", "cherry"]"#, + ValidateDataResponse::Valid ; + "valid array of strings" +)] +#[test_case( + SCHEMA_ARRAY_OF_STRINGS, + r#"[1, 2, 3]"#, + ValidateDataResponse::Invalid { msg: "Data structure does not match the basic schema types.".to_string() } ; + "invalid array of non-strings for string array schema" +)] +#[test_case( + SCHEMA_ARRAY_OF_OBJECTS, + r#"[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]"#, + ValidateDataResponse::Valid ; + "valid array of objects with required fields" +)] +#[test_case( + SCHEMA_ARRAY_OF_OBJECTS, + r#"[{"id": "one", "name": "Alice"}, {"id": 2, "name": "Bob"}]"#, + ValidateDataResponse::Invalid { msg: "Data structure does not match the basic schema types.".to_string() } ; + "invalid array of objects with wrong type for id field" +)] +#[test_case( + SCHEMA_SIMPLE_OBJECT, + r#"{"title": "Introduction", "count": 10}"#, + ValidateDataResponse::Valid ; + "valid simple object with required fields" +)] +#[test_case( + SCHEMA_SIMPLE_OBJECT, + r#"{"title": "Introduction"}"#, + ValidateDataResponse::Invalid { msg: "Data structure does not match the basic schema types.".to_string() } ; + "missing required field in simple object" +)] +#[test_case( + SCHEMA_SIMPLE_OBJECT, + r#"{"title": "Introduction", "count": "not_a_number"}"#, + ValidateDataResponse::Invalid { msg: "Data structure does not match the basic schema types.".to_string() } ; + "invalid type for count in simple object" +)] +#[test_case( + SCHEMA_NESTED_OBJECT, + r#"{ + "user": { + "name": "Charlie", + "age": 25 + }, + "roles": ["admin", "user"] + }"#, + ValidateDataResponse::Valid ; + "valid nested object with required properties" +)] +#[test_case( + SCHEMA_NESTED_OBJECT, + r#"{ + "user": { + "name": "Charlie" + }, + "roles": ["admin", "user"] + }"#, + ValidateDataResponse::Invalid { msg: "Data structure does not match the basic schema types.".to_string() } ; + "missing required field in nested object" +)] +#[test_case( + SCHEMA_NESTED_OBJECT, + r#"{ + "user": { + "name": "Charlie", + "age": "twenty-five" + }, + "roles": ["admin", "user"] + }"#, + ValidateDataResponse::Invalid { msg: "Data structure does not match the basic schema types.".to_string() } ; + "invalid type for age in nested object" +)] +#[test_case( + SCHEMA_NESTED_OBJECT, + r#"{ + "user": { + "name": "Charlie", + "age": 25 + }, + "roles": ["admin", 123] + }"#, + ValidateDataResponse::Invalid { msg: "Data structure does not match the basic schema types.".to_string() } ; + "invalid type in roles array within nested object" +)] +fn test_basic_type_matches_cases(schema: &str, data: &str, expected_res: ValidateDataResponse) { + let (deps, _) = proper_initialization(schema.to_string()); + let query_res = query_validate_data(deps.as_ref(), data.to_string()).unwrap(); + assert_eq!(query_res, expected_res); +} + +pub const SCHEMA_INITIAL: &str = r#" +{ + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "number" } + }, + "required": ["name", "age"] +}"#; + +#[test] +fn test_query_schema() { + let (deps, _) = proper_initialization(SCHEMA_INITIAL.to_string()); + let query_res = query_schema(deps.as_ref()).unwrap(); + let schema = query_res.schema; + assert_eq!( + schema, + "{\"properties\":{\"age\":{\"type\":\"number\"},\"name\":{\"type\":\"string\"}},\"required\":[\"name\",\"age\"],\"type\":\"object\"}".to_string() + ); +} diff --git a/packages/andromeda-data-storage/src/form.rs b/packages/andromeda-data-storage/src/form.rs new file mode 100644 index 000000000..b86c3519c --- /dev/null +++ b/packages/andromeda-data-storage/src/form.rs @@ -0,0 +1,92 @@ +use andromeda_std::{amp::AndrAddr, common::expiration::Expiry}; +use andromeda_std::{andr_exec, andr_instantiate, andr_query}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Addr; + +#[andr_instantiate] +#[cw_serde] +pub struct InstantiateMsg { + pub schema_ado_address: AndrAddr, // Address of the schema ADO + pub authorized_addresses_for_submission: Option>, + pub form_config: FormConfig, + pub custom_key_for_notifications: Option, +} + +#[cw_serde] +pub struct FormConfig { + pub start_time: Option, // Optional start time for form + pub end_time: Option, // Optional end time for form + pub allow_multiple_submissions: bool, // Whether multiple submissions are allowed + pub allow_edit_submission: bool, // Whether users can edit their submission +} + +#[andr_exec] +#[cw_serde] +pub enum ExecuteMsg { + SubmitForm { + data: String, + }, + DeleteSubmission { + submission_id: u64, + wallet_address: AndrAddr, + }, + EditSubmission { + submission_id: u64, + wallet_address: AndrAddr, + data: String, + }, + OpenForm {}, + CloseForm {}, +} + +#[andr_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(GetSchemaResponse)] + GetSchema {}, + #[returns(GetAllSubmissionsResponse)] + GetAllSubmissions {}, + #[returns(GetSubmissionResponse)] + GetSubmission { + submission_id: u64, + wallet_address: AndrAddr, + }, + #[returns(GetSubmissionIdsResponse)] + GetSubmissionIds { wallet_address: AndrAddr }, + #[returns(GetFormStatusResponse)] + GetFormStatus {}, +} + +#[cw_serde] +pub struct GetSchemaResponse { + pub schema: String, +} + +#[cw_serde] +pub struct GetAllSubmissionsResponse { + pub all_submissions: Vec, +} + +#[cw_serde] +pub struct GetSubmissionResponse { + pub submission: Option, +} + +#[cw_serde] +pub struct GetSubmissionIdsResponse { + pub submission_ids: Vec, +} + +#[cw_serde] +pub struct SubmissionInfo { + pub submission_id: u64, + pub wallet_address: Addr, + pub data: String, +} + +#[cw_serde] +pub enum GetFormStatusResponse { + Opened, + Closed, +} diff --git a/packages/andromeda-data-storage/src/lib.rs b/packages/andromeda-data-storage/src/lib.rs index 35859a631..6571b6fe8 100644 --- a/packages/andromeda-data-storage/src/lib.rs +++ b/packages/andromeda-data-storage/src/lib.rs @@ -1,3 +1,4 @@ pub mod boolean; +pub mod form; pub mod primitive; pub mod string_storage; diff --git a/packages/andromeda-modules/src/lib.rs b/packages/andromeda-modules/src/lib.rs index 600952077..877440ce5 100644 --- a/packages/andromeda-modules/src/lib.rs +++ b/packages/andromeda-modules/src/lib.rs @@ -1,2 +1,3 @@ pub mod address_list; pub mod rates; +pub mod schema; diff --git a/packages/andromeda-modules/src/schema.rs b/packages/andromeda-modules/src/schema.rs new file mode 100644 index 000000000..8636a66ee --- /dev/null +++ b/packages/andromeda-modules/src/schema.rs @@ -0,0 +1,35 @@ +use andromeda_std::{andr_exec, andr_instantiate, andr_query}; +use cosmwasm_schema::{cw_serde, QueryResponses}; + +#[andr_instantiate] +#[cw_serde] +pub struct InstantiateMsg { + pub schema_json_string: String, +} + +#[andr_exec] +#[cw_serde] +pub enum ExecuteMsg { + UpdateSchema { new_schema_json_string: String }, +} + +#[andr_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(ValidateDataResponse)] + ValidateData { data: String }, + #[returns(GetSchemaResponse)] + GetSchema {}, +} + +#[cw_serde] +pub enum ValidateDataResponse { + Valid, + Invalid { msg: String }, +} + +#[cw_serde] +pub struct GetSchemaResponse { + pub schema: String, +} From 9e2da583b03a8d4760d103784268e4698cd9adf8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:19:00 +0000 Subject: [PATCH 6/9] build(deps): bump serde from 1.0.215 to 1.0.216 (#718) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- contracts/finance/andromeda-cross-chain-swap/Cargo.toml | 2 +- contracts/fungible-tokens/andromeda-ics20/Cargo.toml | 2 +- .../fungible-tokens/andromeda-merkle-airdrop/Cargo.toml | 2 +- contracts/os/andromeda-vfs/Cargo.toml | 2 +- packages/andromeda-data-storage/Cargo.toml | 2 +- packages/andromeda-finance/Cargo.toml | 2 +- packages/andromeda-fungible-tokens/Cargo.toml | 2 +- packages/andromeda-modules/Cargo.toml | 2 +- packages/andromeda-non-fungible-tokens/Cargo.toml | 2 +- packages/deploy/Cargo.toml | 2 +- packages/std/Cargo.toml | 2 +- 13 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94241575b..129df07e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5055,9 +5055,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] @@ -5100,9 +5100,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 605cdb625..2d415633d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ cosmwasm-schema = "1.5.8" semver = "1.0.0" enum-repr = "0.2.6" cw-multi-test = { version = "1.0.0", features = ["cosmwasm_1_2"] } -serde = { version = "1.0.215" } +serde = { version = "1.0.216" } test-case = { version = "3.3.1" } cw-orch = "=0.24.1" jsonschema-valid = { version = "0.5.2"} diff --git a/contracts/finance/andromeda-cross-chain-swap/Cargo.toml b/contracts/finance/andromeda-cross-chain-swap/Cargo.toml index 96b5b41c8..98cf2416c 100644 --- a/contracts/finance/andromeda-cross-chain-swap/Cargo.toml +++ b/contracts/finance/andromeda-cross-chain-swap/Cargo.toml @@ -21,7 +21,7 @@ cosmwasm-schema = { workspace = true } cw-storage-plus = { workspace = true } cw-utils = { workspace = true } schemars = { version = "0.8.10" } -serde = { version = "1.0.215", default-features = false, features = ["derive"] } +serde = { version = "1.0.216", default-features = false, features = ["derive"] } semver = { workspace = true } andromeda-std = { workspace = true } diff --git a/contracts/fungible-tokens/andromeda-ics20/Cargo.toml b/contracts/fungible-tokens/andromeda-ics20/Cargo.toml index 71d69e0aa..9a90e283c 100644 --- a/contracts/fungible-tokens/andromeda-ics20/Cargo.toml +++ b/contracts/fungible-tokens/andromeda-ics20/Cargo.toml @@ -26,7 +26,7 @@ semver = { workspace = true } cw20-base = { workspace = true } cw-controllers = "=1.1.2" schemars = "0.8.21" -serde = "1.0.215" +serde = "1.0.216" thiserror = "2.0.6" andromeda-std = { workspace = true } diff --git a/contracts/fungible-tokens/andromeda-merkle-airdrop/Cargo.toml b/contracts/fungible-tokens/andromeda-merkle-airdrop/Cargo.toml index a823fe079..fdb84bf29 100644 --- a/contracts/fungible-tokens/andromeda-merkle-airdrop/Cargo.toml +++ b/contracts/fungible-tokens/andromeda-merkle-airdrop/Cargo.toml @@ -24,7 +24,7 @@ cw20 = { workspace = true } cw-asset = { workspace = true } sha2 = "0.10.6" hex = "0.4.3" -serde = "1.0.215" +serde = "1.0.216" andromeda-std = { workspace = true } andromeda-fungible-tokens = { workspace = true } diff --git a/contracts/os/andromeda-vfs/Cargo.toml b/contracts/os/andromeda-vfs/Cargo.toml index 4620018ef..27437eb5c 100644 --- a/contracts/os/andromeda-vfs/Cargo.toml +++ b/contracts/os/andromeda-vfs/Cargo.toml @@ -25,7 +25,7 @@ testing = ["cw-multi-test"] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } cw-storage-plus = { workspace = true } -serde = { version = "1.0.215", default-features = false, features = ["derive"] } +serde = { version = "1.0.216", default-features = false, features = ["derive"] } andromeda-std = { workspace = true } diff --git a/packages/andromeda-data-storage/Cargo.toml b/packages/andromeda-data-storage/Cargo.toml index 94018f948..71f3e033c 100644 --- a/packages/andromeda-data-storage/Cargo.toml +++ b/packages/andromeda-data-storage/Cargo.toml @@ -16,7 +16,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } -serde = { version = "1.0.215", default-features = false, features = ["derive"] } +serde = { version = "1.0.216", default-features = false, features = ["derive"] } cw-utils = { workspace = true } andromeda-std = { workspace = true, features = [] } diff --git a/packages/andromeda-finance/Cargo.toml b/packages/andromeda-finance/Cargo.toml index c5fe1c76d..14386e0d5 100644 --- a/packages/andromeda-finance/Cargo.toml +++ b/packages/andromeda-finance/Cargo.toml @@ -17,7 +17,7 @@ testing = ["cw-multi-test"] [dependencies] cosmwasm-std = { workspace = true, features = ["staking"] } cosmwasm-schema = { workspace = true } -serde = { version = "1.0.215", default-features = false, features = ["derive"] } +serde = { version = "1.0.216", default-features = false, features = ["derive"] } cw-utils = { workspace = true } cw3 = { workspace = true } cw4 = { workspace = true } diff --git a/packages/andromeda-fungible-tokens/Cargo.toml b/packages/andromeda-fungible-tokens/Cargo.toml index bd892e68c..f203a6eba 100644 --- a/packages/andromeda-fungible-tokens/Cargo.toml +++ b/packages/andromeda-fungible-tokens/Cargo.toml @@ -17,7 +17,7 @@ testing = ["cw-multi-test"] [dependencies] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } -serde = { version = "1.0.215", default-features = false, features = ["derive"] } +serde = { version = "1.0.216", default-features = false, features = ["derive"] } cw-utils = { workspace = true } cw20 = { workspace = true } cw20-base = { workspace = true } diff --git a/packages/andromeda-modules/Cargo.toml b/packages/andromeda-modules/Cargo.toml index d84f18302..ff44ed0cf 100644 --- a/packages/andromeda-modules/Cargo.toml +++ b/packages/andromeda-modules/Cargo.toml @@ -16,7 +16,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } -serde = { version = "1.0.215", default-features = false, features = ["derive"] } +serde = { version = "1.0.216", default-features = false, features = ["derive"] } cw-utils = { workspace = true } cw721 = { workspace = true } cw721-base = { workspace = true } diff --git a/packages/andromeda-non-fungible-tokens/Cargo.toml b/packages/andromeda-non-fungible-tokens/Cargo.toml index ddf767d70..6166a0975 100644 --- a/packages/andromeda-non-fungible-tokens/Cargo.toml +++ b/packages/andromeda-non-fungible-tokens/Cargo.toml @@ -17,7 +17,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } -serde = { version = "1.0.215", default-features = false, features = ["derive"] } +serde = { version = "1.0.216", default-features = false, features = ["derive"] } cw-utils = { workspace = true } cw721 = { workspace = true } cw721-base = { workspace = true } diff --git a/packages/deploy/Cargo.toml b/packages/deploy/Cargo.toml index 57168540f..3d55c33f7 100644 --- a/packages/deploy/Cargo.toml +++ b/packages/deploy/Cargo.toml @@ -14,7 +14,7 @@ cosmwasm-std = { workspace = true, features = ["ibc3", "cosmwasm_1_2"] } cosmwasm-schema = { workspace = true } cw-storage-plus = { workspace = true } schemars = "0.8.10" -serde = { version = "1.0.215", default-features = false, features = ["derive"] } +serde = { version = "1.0.216", default-features = false, features = ["derive"] } semver = { workspace = true } cw20 = { version = "1.0.1" } cw20-base = { workspace = true, features = ["library"] } diff --git a/packages/std/Cargo.toml b/packages/std/Cargo.toml index 9ac7e384a..d975ae343 100644 --- a/packages/std/Cargo.toml +++ b/packages/std/Cargo.toml @@ -21,7 +21,7 @@ cosmwasm-std = { workspace = true, features = ["ibc3", "cosmwasm_1_2"] } cosmwasm-schema = { workspace = true } cw-storage-plus = { workspace = true } schemars = "0.8.10" -serde = { version = "1.0.215", default-features = false, features = ["derive"] } +serde = { version = "1.0.216", default-features = false, features = ["derive"] } semver = { workspace = true } cw20 = { version = "1.0.1" } cw20-base = { workspace = true, features = ["library"] } From c507ad915a010f90f4bc1320b3d4287e17f2195e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:24:25 +0000 Subject: [PATCH 7/9] build(deps): bump semver from 1.0.23 to 1.0.24 (#719) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 129df07e1..d2f10aa5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5049,9 +5049,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" diff --git a/Cargo.toml b/Cargo.toml index 2d415633d..c56971e1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ cw721 = "0.18.0" cw721-base = { version = "0.18.0", features = ["library"] } cw-asset = "=3.0.0" cosmwasm-schema = "1.5.8" -semver = "1.0.0" +semver = "1.0.24" enum-repr = "0.2.6" cw-multi-test = { version = "1.0.0", features = ["cosmwasm_1_2"] } serde = { version = "1.0.216" } From ff613a3d94c165e32c25851dbc6e0c5f25b1592b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:25:03 +0000 Subject: [PATCH 8/9] build(deps): bump thiserror from 2.0.6 to 2.0.7 (#722) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 16 ++++++++-------- .../fungible-tokens/andromeda-ics20/Cargo.toml | 2 +- packages/deploy/Cargo.toml | 2 +- packages/std/Cargo.toml | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2f10aa5c..9373d340c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -450,7 +450,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "strum_macros", - "thiserror 2.0.6", + "thiserror 2.0.7", ] [[package]] @@ -624,7 +624,7 @@ dependencies = [ "schemars", "semver", "serde", - "thiserror 2.0.6", + "thiserror 2.0.7", ] [[package]] @@ -935,7 +935,7 @@ dependencies = [ "serde-json-wasm 1.0.1", "sha2 0.10.8", "strum_macros", - "thiserror 2.0.6", + "thiserror 2.0.7", ] [[package]] @@ -5747,11 +5747,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" dependencies = [ - "thiserror-impl 2.0.6", + "thiserror-impl 2.0.7", ] [[package]] @@ -5767,9 +5767,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" dependencies = [ "proc-macro2", "quote", diff --git a/contracts/fungible-tokens/andromeda-ics20/Cargo.toml b/contracts/fungible-tokens/andromeda-ics20/Cargo.toml index 9a90e283c..510555561 100644 --- a/contracts/fungible-tokens/andromeda-ics20/Cargo.toml +++ b/contracts/fungible-tokens/andromeda-ics20/Cargo.toml @@ -27,7 +27,7 @@ cw20-base = { workspace = true } cw-controllers = "=1.1.2" schemars = "0.8.21" serde = "1.0.216" -thiserror = "2.0.6" +thiserror = "2.0.7" andromeda-std = { workspace = true } andromeda-fungible-tokens = { workspace = true } diff --git a/packages/deploy/Cargo.toml b/packages/deploy/Cargo.toml index 3d55c33f7..16666f6f1 100644 --- a/packages/deploy/Cargo.toml +++ b/packages/deploy/Cargo.toml @@ -22,7 +22,7 @@ cw721-base = { workspace = true } cw-utils = { workspace = true } cw2 = { workspace = true } cw-asset = { version = "3.0.0" } -thiserror = { version = "2.0.6" } +thiserror = { version = "2.0.7" } lazy_static = "1" hex = "0.4" regex = { version = "1.9.1", default-features = false } diff --git a/packages/std/Cargo.toml b/packages/std/Cargo.toml index d975ae343..2242f40ac 100644 --- a/packages/std/Cargo.toml +++ b/packages/std/Cargo.toml @@ -30,7 +30,7 @@ cw-utils = { workspace = true } cw2 = { workspace = true } cw3 = { workspace = true } cw-asset = { version = "3.0.0" } -thiserror = { version = "2.0.6" } +thiserror = { version = "2.0.7" } lazy_static = "1" hex = "0.4" regex = { version = "1.9.1", default-features = false } From 07c57fbcfc139256acafd7c1a57945458adaf2d6 Mon Sep 17 00:00:00 2001 From: Joe Monem <66594578+joemonem@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:40:48 +0200 Subject: [PATCH 9/9] feat: cross chain recipients no longer error in get_raw_address_from_vfs (#725) --- CHANGELOG.md | 1 + Cargo.lock | 2 +- .../data-storage/andromeda-form/src/state.rs | 2 +- .../src/testing/mock_querier.rs | 1 + .../src/testing/mock_querier.rs | 1 + .../src/testing/mock_querier.rs | 1 + .../src/testing/mock_querier.rs | 1 + .../src/testing/mock_querier.rs | 1 + .../finance/andromeda-timelock/src/state.rs | 2 +- .../src/testing/mock_querier.rs | 1 + .../finance/andromeda-vesting/src/state.rs | 2 +- .../src/testing/mock_querier.rs | 50 +- .../src/testing/mock_querier.rs | 1 + .../src/testing/mock_querier.rs | 1 + .../src/testing/mock_querier.rs | 1 + .../andromeda-matrix/src/testing/tests.rs | 2 +- .../src/testing/mock_querier.rs | 1 + .../src/testing/mock_querier.rs | 1 + .../andromeda-auction/src/contract.rs | 6 +- .../andromeda-auction/src/state.rs | 2 +- .../src/testing/mock_querier.rs | 1 + .../andromeda-marketplace/src/contract.rs | 6 +- .../andromeda-marketplace/src/state.rs | 2 +- .../src/testing/mock_querier.rs | 1 + contracts/os/andromeda-adodb/src/tests.rs | 6 +- contracts/os/andromeda-vfs/src/state.rs | 2 +- .../src/string_storage.rs | 2 +- .../src/crowdfund.rs | 15 +- packages/deploy/src/os.rs | 2 +- packages/std/Cargo.toml | 2 +- packages/std/src/ado_base/rates.rs | 28 +- packages/std/src/ado_contract/app.rs | 2 +- packages/std/src/ado_contract/execute.rs | 2 +- packages/std/src/ado_contract/ownership.rs | 2 +- .../std/src/ado_contract/permissioning.rs | 4 +- packages/std/src/ado_contract/query.rs | 4 +- packages/std/src/ado_contract/rates.rs | 2 +- packages/std/src/ado_contract/state.rs | 2 +- packages/std/src/amp/addresses.rs | 24 +- packages/std/src/amp/recipient.rs | 5 +- packages/std/src/common/context.rs | 2 +- tests-integration/tests/kernel_orch.rs | 65 --- tests-integration/tests/rates_orch.rs | 2 +- tests-integration/tests/splitter.rs | 436 +++++++++++++++++- 44 files changed, 517 insertions(+), 182 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e017edb2..5f07553e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Rates: Limit rates recipient to only one address [(#669)](https://github.com/andromedaprotocol/andromeda-core/pull/669) +- Address Validation: Cross-chain recipients don't need to be registered in VFS [(#725)](https://github.com/andromedaprotocol/andromeda-core/pull/725) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 9373d340c..c83b520b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -907,7 +907,7 @@ dependencies = [ [[package]] name = "andromeda-std" -version = "1.5.0-b.1" +version = "1.5.0-b.2" dependencies = [ "andromeda-macros", "cosmwasm-schema 1.5.8", diff --git a/contracts/data-storage/andromeda-form/src/state.rs b/contracts/data-storage/andromeda-form/src/state.rs index bc661c871..06a049ea0 100644 --- a/contracts/data-storage/andromeda-form/src/state.rs +++ b/contracts/data-storage/andromeda-form/src/state.rs @@ -26,7 +26,7 @@ pub struct SubmissionIndexes<'a> { pub wallet_address: MultiIndex<'a, Addr, SubmissionInfo, (u64, Addr)>, } -impl<'a> IndexList for SubmissionIndexes<'a> { +impl IndexList for SubmissionIndexes<'_> { fn get_indexes( &'_ self, ) -> Box> + '_> { diff --git a/contracts/finance/andromeda-conditional-splitter/src/testing/mock_querier.rs b/contracts/finance/andromeda-conditional-splitter/src/testing/mock_querier.rs index abe4b9ad9..fec73ad8e 100644 --- a/contracts/finance/andromeda-conditional-splitter/src/testing/mock_querier.rs +++ b/contracts/finance/andromeda-conditional-splitter/src/testing/mock_querier.rs @@ -45,6 +45,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/finance/andromeda-cross-chain-swap/src/testing/mock_querier.rs b/contracts/finance/andromeda-cross-chain-swap/src/testing/mock_querier.rs index abe4b9ad9..fec73ad8e 100644 --- a/contracts/finance/andromeda-cross-chain-swap/src/testing/mock_querier.rs +++ b/contracts/finance/andromeda-cross-chain-swap/src/testing/mock_querier.rs @@ -45,6 +45,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/finance/andromeda-rate-limiting-withdrawals/src/testing/mock_querier.rs b/contracts/finance/andromeda-rate-limiting-withdrawals/src/testing/mock_querier.rs index ffd951f86..ee3627ea3 100644 --- a/contracts/finance/andromeda-rate-limiting-withdrawals/src/testing/mock_querier.rs +++ b/contracts/finance/andromeda-rate-limiting-withdrawals/src/testing/mock_querier.rs @@ -47,6 +47,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/finance/andromeda-set-amount-splitter/src/testing/mock_querier.rs b/contracts/finance/andromeda-set-amount-splitter/src/testing/mock_querier.rs index d7d944bad..865dacede 100644 --- a/contracts/finance/andromeda-set-amount-splitter/src/testing/mock_querier.rs +++ b/contracts/finance/andromeda-set-amount-splitter/src/testing/mock_querier.rs @@ -45,6 +45,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/finance/andromeda-splitter/src/testing/mock_querier.rs b/contracts/finance/andromeda-splitter/src/testing/mock_querier.rs index d7d944bad..865dacede 100644 --- a/contracts/finance/andromeda-splitter/src/testing/mock_querier.rs +++ b/contracts/finance/andromeda-splitter/src/testing/mock_querier.rs @@ -45,6 +45,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/finance/andromeda-timelock/src/state.rs b/contracts/finance/andromeda-timelock/src/state.rs index cf0aa69f2..54796e9fa 100644 --- a/contracts/finance/andromeda-timelock/src/state.rs +++ b/contracts/finance/andromeda-timelock/src/state.rs @@ -11,7 +11,7 @@ pub struct EscrowIndexes<'a> { pub owner: MultiIndex<'a, String, Escrow, Vec>, } -impl<'a> IndexList for EscrowIndexes<'a> { +impl IndexList for EscrowIndexes<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.owner]; Box::new(v.into_iter()) diff --git a/contracts/finance/andromeda-timelock/src/testing/mock_querier.rs b/contracts/finance/andromeda-timelock/src/testing/mock_querier.rs index 970f59b2a..d250753a4 100644 --- a/contracts/finance/andromeda-timelock/src/testing/mock_querier.rs +++ b/contracts/finance/andromeda-timelock/src/testing/mock_querier.rs @@ -44,6 +44,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/finance/andromeda-vesting/src/state.rs b/contracts/finance/andromeda-vesting/src/state.rs index c82a08ba4..9fc1d1f5a 100644 --- a/contracts/finance/andromeda-vesting/src/state.rs +++ b/contracts/finance/andromeda-vesting/src/state.rs @@ -38,7 +38,7 @@ pub struct BatchIndexes<'a> { pub claim_time: MultiIndex<'a, (u8, u64), Batch, u64>, } -impl<'a> IndexList for BatchIndexes<'a> { +impl IndexList for BatchIndexes<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.claim_time]; Box::new(v.into_iter()) diff --git a/contracts/finance/andromeda-weighted-distribution-splitter/src/testing/mock_querier.rs b/contracts/finance/andromeda-weighted-distribution-splitter/src/testing/mock_querier.rs index d7d944bad..6965c7a47 100644 --- a/contracts/finance/andromeda-weighted-distribution-splitter/src/testing/mock_querier.rs +++ b/contracts/finance/andromeda-weighted-distribution-splitter/src/testing/mock_querier.rs @@ -1,50 +1,14 @@ -use andromeda_std::ado_base::InstantiateMsg; -use andromeda_std::ado_contract::ADOContract; use andromeda_std::testing::mock_querier::MockAndromedaQuerier; -use cosmwasm_std::testing::mock_info; -use cosmwasm_std::QuerierWrapper; use cosmwasm_std::{ - from_json, - testing::{mock_env, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}, - Coin, OwnedDeps, Querier, QuerierResult, QueryRequest, SystemError, SystemResult, WasmQuery, + from_json, testing::MockQuerier, Querier, QuerierResult, QueryRequest, SystemError, + SystemResult, WasmQuery, }; -pub use andromeda_std::testing::mock_querier::MOCK_KERNEL_CONTRACT; - /// Alternative to `cosmwasm_std::testing::mock_dependencies` that allows us to respond to custom queries. /// /// Automatically assigns a kernel address as MOCK_KERNEL_CONTRACT. -pub fn mock_dependencies_custom( - contract_balance: &[Coin], -) -> OwnedDeps { - let custom_querier: WasmMockQuerier = - WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); - let storage = MockStorage::default(); - let mut deps = OwnedDeps { - storage, - api: MockApi::default(), - querier: custom_querier, - custom_query_type: std::marker::PhantomData, - }; - ADOContract::default() - .instantiate( - &mut deps.storage, - mock_env(), - &deps.api, - &QuerierWrapper::new(&deps.querier), - mock_info("sender", &[]), - InstantiateMsg { - ado_type: "splitter".to_string(), - ado_version: "test".to_string(), - - kernel_address: MOCK_KERNEL_CONTRACT.to_string(), - owner: None, - }, - ) - .unwrap(); - deps -} +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, @@ -80,12 +44,4 @@ impl WasmMockQuerier { _ => MockAndromedaQuerier::default().handle_query(&self.base, request), } } - - pub fn new(base: MockQuerier) -> Self { - WasmMockQuerier { - base, - contract_address: mock_env().contract.address.to_string(), - tokens_left_to_burn: 2, - } - } } diff --git a/contracts/fungible-tokens/andromeda-cw20/src/testing/mock_querier.rs b/contracts/fungible-tokens/andromeda-cw20/src/testing/mock_querier.rs index b47be8463..f68cf8a51 100644 --- a/contracts/fungible-tokens/andromeda-cw20/src/testing/mock_querier.rs +++ b/contracts/fungible-tokens/andromeda-cw20/src/testing/mock_querier.rs @@ -43,6 +43,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/fungible-tokens/andromeda-ics20/src/testing/mock_querier.rs b/contracts/fungible-tokens/andromeda-ics20/src/testing/mock_querier.rs index b47be8463..f68cf8a51 100644 --- a/contracts/fungible-tokens/andromeda-ics20/src/testing/mock_querier.rs +++ b/contracts/fungible-tokens/andromeda-ics20/src/testing/mock_querier.rs @@ -43,6 +43,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/fungible-tokens/andromeda-lockdrop/src/testing/mock_querier.rs b/contracts/fungible-tokens/andromeda-lockdrop/src/testing/mock_querier.rs index fd11206b6..3c55b8486 100644 --- a/contracts/fungible-tokens/andromeda-lockdrop/src/testing/mock_querier.rs +++ b/contracts/fungible-tokens/andromeda-lockdrop/src/testing/mock_querier.rs @@ -57,6 +57,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/math/andromeda-matrix/src/testing/tests.rs b/contracts/math/andromeda-matrix/src/testing/tests.rs index 07be2a410..04821323b 100644 --- a/contracts/math/andromeda-matrix/src/testing/tests.rs +++ b/contracts/math/andromeda-matrix/src/testing/tests.rs @@ -138,7 +138,7 @@ fn test_authorization() { // Store as external user // This should error - let err = store_matrix(deps.as_mut(), &key, &data, &UNAUTHORIZED_OPERATOR).unwrap_err(); + let err = store_matrix(deps.as_mut(), &key, &data, UNAUTHORIZED_OPERATOR).unwrap_err(); assert_eq!(err, ContractError::Unauthorized {}); } diff --git a/contracts/modules/andromeda-address-list/src/testing/mock_querier.rs b/contracts/modules/andromeda-address-list/src/testing/mock_querier.rs index 0479281a1..49e88098f 100644 --- a/contracts/modules/andromeda-address-list/src/testing/mock_querier.rs +++ b/contracts/modules/andromeda-address-list/src/testing/mock_querier.rs @@ -44,6 +44,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/modules/andromeda-rates/src/testing/mock_querier.rs b/contracts/modules/andromeda-rates/src/testing/mock_querier.rs index 09714db63..acb1b513c 100644 --- a/contracts/modules/andromeda-rates/src/testing/mock_querier.rs +++ b/contracts/modules/andromeda-rates/src/testing/mock_querier.rs @@ -49,6 +49,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/non-fungible-tokens/andromeda-auction/src/contract.rs b/contracts/non-fungible-tokens/andromeda-auction/src/contract.rs index 64be2c107..a963f6a69 100644 --- a/contracts/non-fungible-tokens/andromeda-auction/src/contract.rs +++ b/contracts/non-fungible-tokens/andromeda-auction/src/contract.rs @@ -436,7 +436,7 @@ fn execute_update_auction( token_auction_state.start_time = start_expiration; token_auction_state.end_time = end_expiration; - token_auction_state.coin_denom = coin_denom.clone(); + token_auction_state.coin_denom.clone_from(&coin_denom); token_auction_state.uses_cw20 = uses_cw20; token_auction_state.min_bid = min_bid; token_auction_state.min_raise = min_raise; @@ -1182,8 +1182,8 @@ fn get_and_increment_next_auction_id( let mut auction_info = auction_infos().load(storage, &key).unwrap_or_default(); auction_info.push(next_auction_id); if auction_info.token_address.is_empty() { - auction_info.token_address = token_address.to_owned(); - auction_info.token_id = token_id.to_owned(); + token_address.clone_into(&mut auction_info.token_address); + token_id.clone_into(&mut auction_info.token_id); } auction_infos().save(storage, &key, &auction_info)?; Ok(next_auction_id) diff --git a/contracts/non-fungible-tokens/andromeda-auction/src/state.rs b/contracts/non-fungible-tokens/andromeda-auction/src/state.rs index 3a1ae18d7..e8d6ab700 100644 --- a/contracts/non-fungible-tokens/andromeda-auction/src/state.rs +++ b/contracts/non-fungible-tokens/andromeda-auction/src/state.rs @@ -21,7 +21,7 @@ pub struct AuctionIdIndices<'a> { pub token: MultiIndex<'a, String, AuctionInfo, String>, } -impl<'a> IndexList for AuctionIdIndices<'a> { +impl IndexList for AuctionIdIndices<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.token]; Box::new(v.into_iter()) diff --git a/contracts/non-fungible-tokens/andromeda-auction/src/testing/mock_querier.rs b/contracts/non-fungible-tokens/andromeda-auction/src/testing/mock_querier.rs index 214e8aa96..b1ebceef4 100644 --- a/contracts/non-fungible-tokens/andromeda-auction/src/testing/mock_querier.rs +++ b/contracts/non-fungible-tokens/andromeda-auction/src/testing/mock_querier.rs @@ -61,6 +61,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/non-fungible-tokens/andromeda-marketplace/src/contract.rs b/contracts/non-fungible-tokens/andromeda-marketplace/src/contract.rs index 11d8fa5fd..ce175ce26 100644 --- a/contracts/non-fungible-tokens/andromeda-marketplace/src/contract.rs +++ b/contracts/non-fungible-tokens/andromeda-marketplace/src/contract.rs @@ -311,7 +311,7 @@ fn execute_update_sale( ensure!(price > Uint128::zero(), ContractError::InvalidZeroAmount {}); token_sale_state.price = price; - token_sale_state.coin_denom = coin_denom.clone(); + token_sale_state.coin_denom.clone_from(&coin_denom); token_sale_state.uses_cw20 = uses_cw20; token_sale_state.recipient = recipient; TOKEN_SALE_STATE.save( @@ -779,8 +779,8 @@ fn get_and_increment_next_sale_id( let mut sale_info = sale_infos().load(storage, &key).unwrap_or_default(); sale_info.push(next_sale_id); if sale_info.token_address.is_empty() { - sale_info.token_address = token_address.to_owned(); - sale_info.token_id = token_id.to_owned(); + token_address.clone_into(&mut sale_info.token_address); + token_id.clone_into(&mut sale_info.token_id); } sale_infos().save(storage, &key, &sale_info)?; Ok(next_sale_id) diff --git a/contracts/non-fungible-tokens/andromeda-marketplace/src/state.rs b/contracts/non-fungible-tokens/andromeda-marketplace/src/state.rs index 4c8b7dedb..5cca55bc9 100644 --- a/contracts/non-fungible-tokens/andromeda-marketplace/src/state.rs +++ b/contracts/non-fungible-tokens/andromeda-marketplace/src/state.rs @@ -78,7 +78,7 @@ pub struct SaleIdIndices<'a> { pub token: MultiIndex<'a, String, SaleInfo, String>, } -impl<'a> IndexList for SaleIdIndices<'a> { +impl IndexList for SaleIdIndices<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.token]; Box::new(v.into_iter()) diff --git a/contracts/non-fungible-tokens/andromeda-marketplace/src/testing/mock_querier.rs b/contracts/non-fungible-tokens/andromeda-marketplace/src/testing/mock_querier.rs index 644b0cca2..5517c62c3 100644 --- a/contracts/non-fungible-tokens/andromeda-marketplace/src/testing/mock_querier.rs +++ b/contracts/non-fungible-tokens/andromeda-marketplace/src/testing/mock_querier.rs @@ -54,6 +54,7 @@ pub fn mock_dependencies_custom( deps } +#[allow(dead_code)] pub struct WasmMockQuerier { pub base: MockQuerier, pub contract_address: String, diff --git a/contracts/os/andromeda-adodb/src/tests.rs b/contracts/os/andromeda-adodb/src/tests.rs index a0b729abf..c4e75a1e2 100644 --- a/contracts/os/andromeda-adodb/src/tests.rs +++ b/contracts/os/andromeda-adodb/src/tests.rs @@ -718,11 +718,9 @@ fn test_all_ado_types() { let mut code_id = 1; - let ados = vec![ - ADOVersion::from_string("ado_type_1@0.1.0".to_string()), + let ados = [ADOVersion::from_string("ado_type_1@0.1.0".to_string()), ADOVersion::from_string("ado_type_1@0.1.1".to_string()), - ADOVersion::from_string("ado_type_2@0.1.0".to_string()), - ]; + ADOVersion::from_string("ado_type_2@0.1.0".to_string())]; ados.iter().for_each(|ado_version| { let msg = ExecuteMsg::Publish { diff --git a/contracts/os/andromeda-vfs/src/state.rs b/contracts/os/andromeda-vfs/src/state.rs index 7e2378ac7..3d824196c 100644 --- a/contracts/os/andromeda-vfs/src/state.rs +++ b/contracts/os/andromeda-vfs/src/state.rs @@ -25,7 +25,7 @@ pub struct PathIndices<'a> { pub parent: MultiIndex<'a, Addr, PathInfo, (Addr, String)>, } -impl<'a> IndexList for PathIndices<'a> { +impl IndexList for PathIndices<'_> { fn get_indexes( &'_ self, ) -> Box> + '_> { diff --git a/packages/andromeda-data-storage/src/string_storage.rs b/packages/andromeda-data-storage/src/string_storage.rs index 34d36b9d9..64eec7dfb 100644 --- a/packages/andromeda-data-storage/src/string_storage.rs +++ b/packages/andromeda-data-storage/src/string_storage.rs @@ -97,7 +97,7 @@ mod tests { #[test] fn test_from_string() { - let cases = vec![( + let cases = [( StringStorage::String("String".to_string()), "String".to_string(), )]; diff --git a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs index baf1886cf..fda382d34 100644 --- a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs +++ b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs @@ -122,15 +122,14 @@ pub enum CampaignStage { DISCARDED, } -impl ToString for CampaignStage { - #[inline] - fn to_string(&self) -> String { +impl std::fmt::Display for CampaignStage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::READY => "READY".to_string(), - Self::ONGOING => "ONGOING".to_string(), - Self::SUCCESS => "SUCCESS".to_string(), - Self::FAILED => "FAILED".to_string(), - Self::DISCARDED => "DISCARDED".to_string(), + Self::READY => write!(f, "READY"), + Self::ONGOING => write!(f, "ONGOING"), + Self::SUCCESS => write!(f, "SUCCESS"), + Self::FAILED => write!(f, "FAILED"), + Self::DISCARDED => write!(f, "DISCARDED"), } } } diff --git a/packages/deploy/src/os.rs b/packages/deploy/src/os.rs index 58b526c72..069ff95fb 100644 --- a/packages/deploy/src/os.rs +++ b/packages/deploy/src/os.rs @@ -151,7 +151,7 @@ impl OperatingSystemDeployment { let deployable = os_contracts(); for (name, contract) in uploaded_contracts { let (_, version, _) = deployable.iter().find(|(n, _, _)| n == name).unwrap(); - let versions = self.adodb.ado_versions(&name.to_string(), None, None)?; + let versions = self.adodb.ado_versions(name.to_string(), None, None)?; if versions.contains(&format!("{}@{}", name, version)) { log::info!( "Skipping publishing {} {} - already published", diff --git a/packages/std/Cargo.toml b/packages/std/Cargo.toml index 2242f40ac..9a267f7b6 100644 --- a/packages/std/Cargo.toml +++ b/packages/std/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "andromeda-std" -version = "1.5.0-b.1" +version = "1.5.0-b.2" edition = "2021" rust-version = "1.75.0" description = "The standard library for creating an Andromeda Digital Object" diff --git a/packages/std/src/ado_base/rates.rs b/packages/std/src/ado_base/rates.rs index 0c69bcc46..fb820fdaf 100644 --- a/packages/std/src/ado_base/rates.rs +++ b/packages/std/src/ado_base/rates.rs @@ -51,9 +51,9 @@ pub struct PaymentAttribute { pub receiver: String, } -impl ToString for PaymentAttribute { - fn to_string(&self) -> String { - format!("{}<{}", self.receiver, self.amount) +impl std::fmt::Display for PaymentAttribute { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}<{}", self.receiver, self.amount) } } @@ -222,19 +222,17 @@ impl LocalRate { msg: to_json_binary(&kernel_msg)?, funds: vec![fee.clone()], }) + } else if is_native { + self.recipient + .generate_direct_msg(&deps, vec![fee.clone()])? } else { - if is_native { - self.recipient - .generate_direct_msg(&deps, vec![fee.clone()])? - } else { - self.recipient.generate_msg_cw20( - &deps, - Cw20Coin { - amount: fee.amount, - address: fee.denom.to_string(), - }, - )? - } + self.recipient.generate_msg_cw20( + &deps, + Cw20Coin { + amount: fee.amount, + address: fee.denom.to_string(), + }, + )? }; msgs.push(msg); diff --git a/packages/std/src/ado_contract/app.rs b/packages/std/src/ado_contract/app.rs index 867870712..5dea97223 100644 --- a/packages/std/src/ado_contract/app.rs +++ b/packages/std/src/ado_contract/app.rs @@ -11,7 +11,7 @@ enum AppQueryMsg { GetAddress { name: String }, } -impl<'a> ADOContract<'a> { +impl ADOContract<'_> { #[inline] pub fn get_app_contract(&self, storage: &dyn Storage) -> Result, ContractError> { Ok(self.app_contract.may_load(storage)?) diff --git a/packages/std/src/ado_contract/execute.rs b/packages/std/src/ado_contract/execute.rs index 7988cb8ed..c13be8776 100644 --- a/packages/std/src/ado_contract/execute.rs +++ b/packages/std/src/ado_contract/execute.rs @@ -29,7 +29,7 @@ use serde::Serialize; type ExecuteContextFunction = fn(ExecuteContext, M) -> Result; -impl<'a> ADOContract<'a> { +impl ADOContract<'_> { pub fn instantiate( &self, storage: &mut dyn Storage, diff --git a/packages/std/src/ado_contract/ownership.rs b/packages/std/src/ado_contract/ownership.rs index 9f9280088..cb28b7411 100644 --- a/packages/std/src/ado_contract/ownership.rs +++ b/packages/std/src/ado_contract/ownership.rs @@ -12,7 +12,7 @@ const POTENTIAL_OWNER: Item = Item::new("andr_potential_owner"); const POTENTIAL_OWNER_EXPIRATION: Item = Item::new("andr_potential_owner_expiration"); -impl<'a> ADOContract<'a> { +impl ADOContract<'_> { pub fn execute_ownership( &self, deps: DepsMut, diff --git a/packages/std/src/ado_contract/permissioning.rs b/packages/std/src/ado_contract/permissioning.rs index 421b3365a..28d51e7e8 100644 --- a/packages/std/src/ado_contract/permissioning.rs +++ b/packages/std/src/ado_contract/permissioning.rs @@ -22,7 +22,7 @@ pub struct PermissionsIndices<'a> { pub action: MultiIndex<'a, String, PermissionInfo, String>, } -impl<'a> IndexList for PermissionsIndices<'a> { +impl IndexList for PermissionsIndices<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.action, &self.actor]; Box::new(v.into_iter()) @@ -44,7 +44,7 @@ pub fn permissions<'a>() -> IndexedMap<'a, &'a str, PermissionInfo, PermissionsI IndexedMap::new("andr_permissions", indexes) } -impl<'a> ADOContract<'a> { +impl ADOContract<'_> { pub fn execute_permissioning( &self, ctx: ExecuteContext, diff --git a/packages/std/src/ado_contract/query.rs b/packages/std/src/ado_contract/query.rs index 22738bd61..23470d7c0 100644 --- a/packages/std/src/ado_contract/query.rs +++ b/packages/std/src/ado_contract/query.rs @@ -16,7 +16,7 @@ use cosmwasm_std::{from_json, to_json_binary, Binary, Deps, Env}; use cw2::get_contract_version; use serde::Serialize; -impl<'a> ADOContract<'a> { +impl ADOContract<'_> { #[allow(unreachable_patterns)] pub fn query( &self, @@ -80,7 +80,7 @@ impl<'a> ADOContract<'a> { } } -impl<'a> ADOContract<'a> { +impl ADOContract<'_> { #[inline] pub fn query_contract_owner(&self, deps: Deps) -> Result { let owner = self.owner.load(deps.storage)?; diff --git a/packages/std/src/ado_contract/rates.rs b/packages/std/src/ado_contract/rates.rs index eac9e69c5..bfd08b081 100644 --- a/packages/std/src/ado_contract/rates.rs +++ b/packages/std/src/ado_contract/rates.rs @@ -12,7 +12,7 @@ pub fn rates<'a>() -> Map<'a, &'a str, Rate> { Map::new("rates") } -impl<'a> ADOContract<'a> { +impl ADOContract<'_> { /// Sets rates pub fn set_rates( &self, diff --git a/packages/std/src/ado_contract/state.rs b/packages/std/src/ado_contract/state.rs index c3f8ab98e..6c88f9335 100644 --- a/packages/std/src/ado_contract/state.rs +++ b/packages/std/src/ado_contract/state.rs @@ -16,7 +16,7 @@ pub struct ADOContract<'a> { pub rates: Map<'a, &'a str, Rate>, } -impl<'a> Default for ADOContract<'a> { +impl Default for ADOContract<'_> { fn default() -> Self { ADOContract { owner: Item::new("owner"), diff --git a/packages/std/src/amp/addresses.rs b/packages/std/src/amp/addresses.rs index 9dda57c4a..8e663a4d3 100644 --- a/packages/std/src/amp/addresses.rs +++ b/packages/std/src/amp/addresses.rs @@ -105,14 +105,22 @@ impl AndrAddr { let valid_vfs_path = self.local_path_to_vfs_path(deps.storage, &deps.querier, vfs_contract.clone())?; let vfs_addr = Addr::unchecked(vfs_contract); - vfs_resolve_path(valid_vfs_path.clone(), vfs_addr, &deps.querier) - .ok() - .ok_or(ContractError::InvalidPathname { - error: Some(format!( - "{:?} does not exist in the file system", - valid_vfs_path.0 - )), - }) + match vfs_resolve_path(valid_vfs_path.clone(), vfs_addr, &deps.querier) { + Ok(addr) => Ok(addr), + Err(_) => { + // If the path is cross-chain then we return it as is + if valid_vfs_path.get_protocol().is_some() { + Ok(Addr::unchecked(valid_vfs_path.into_string())) + } else { + Err(ContractError::InvalidPathname { + error: Some(format!( + "{:?} does not exist in the file system", + valid_vfs_path.0 + )), + }) + } + } + } } } } diff --git a/packages/std/src/amp/recipient.rs b/packages/std/src/amp/recipient.rs index 520d306fa..ae11a5b8f 100644 --- a/packages/std/src/amp/recipient.rs +++ b/packages/std/src/amp/recipient.rs @@ -60,10 +60,7 @@ impl Recipient { pub fn is_cross_chain(&self) -> bool { let protocol = self.address.get_protocol(); - match protocol { - Some("ibc") => true, - _ => false, - } + matches!(protocol, Some("ibc")) } /// Generates a direct sub message for the given recipient. diff --git a/packages/std/src/common/context.rs b/packages/std/src/common/context.rs index 53ca3759e..7fab8aaca 100644 --- a/packages/std/src/common/context.rs +++ b/packages/std/src/common/context.rs @@ -8,7 +8,7 @@ pub struct ExecuteContext<'a> { pub amp_ctx: Option, } -impl<'a> ExecuteContext<'a> { +impl ExecuteContext<'_> { #[inline] pub fn new(deps: DepsMut, info: MessageInfo, env: Env) -> ExecuteContext { ExecuteContext { diff --git a/tests-integration/tests/kernel_orch.rs b/tests-integration/tests/kernel_orch.rs index 48f19ad1c..58993d78b 100644 --- a/tests-integration/tests/kernel_orch.rs +++ b/tests-integration/tests/kernel_orch.rs @@ -729,19 +729,6 @@ fn test_kernel_ibc_funds_only() { ) .unwrap(); - adodb_juno - .execute( - &os::adodb::ExecuteMsg::Publish { - code_id: 4, - ado_type: "economics".to_string(), - action_fees: None, - version: "1.1.1".to_string(), - publisher: None, - }, - None, - ) - .unwrap(); - economics_juno .instantiate( &os::economics::InstantiateMsg { @@ -774,32 +761,6 @@ fn test_kernel_ibc_funds_only() { ) .unwrap(); - adodb_osmosis - .execute( - &os::adodb::ExecuteMsg::Publish { - code_id: 2, - ado_type: "counter".to_string(), - action_fees: None, - version: "1.0.2".to_string(), - publisher: None, - }, - None, - ) - .unwrap(); - - adodb_osmosis - .execute( - &os::adodb::ExecuteMsg::Publish { - code_id: 6, - ado_type: "economics".to_string(), - action_fees: None, - version: "1.1.1".to_string(), - publisher: None, - }, - None, - ) - .unwrap(); - kernel_juno .execute( &ExecuteMsg::UpsertKeyAddress { @@ -1275,19 +1236,6 @@ fn test_kernel_ibc_funds_only_multi_hop() { ) .unwrap(); - adodb_osmosis - .execute( - &os::adodb::ExecuteMsg::Publish { - code_id: 2, - ado_type: "counter".to_string(), - action_fees: None, - version: "1.0.2".to_string(), - publisher: None, - }, - None, - ) - .unwrap(); - kernel_juno .execute( &ExecuteMsg::UpsertKeyAddress { @@ -1668,19 +1616,6 @@ fn test_kernel_ibc_funds_and_execute_msg() { ) .unwrap(); - adodb_osmosis - .execute( - &os::adodb::ExecuteMsg::Publish { - code_id: 2, - ado_type: "counter".to_string(), - action_fees: None, - version: "1.0.2".to_string(), - publisher: None, - }, - None, - ) - .unwrap(); - kernel_juno .execute( &ExecuteMsg::UpsertKeyAddress { diff --git a/tests-integration/tests/rates_orch.rs b/tests-integration/tests/rates_orch.rs index 57904e2e7..7046bb181 100644 --- a/tests-integration/tests/rates_orch.rs +++ b/tests-integration/tests/rates_orch.rs @@ -126,7 +126,7 @@ fn test_marketplace_migration() { kernel_address: kernel_juno.address().unwrap().into_string(), owner: Some(sender.clone().into_string().clone()), }; - rates_juno.instantiate(&rates_init_msg, None, None).unwrap(); + rates_juno.instantiate(rates_init_msg, None, None).unwrap(); kernel_juno .execute( diff --git a/tests-integration/tests/splitter.rs b/tests-integration/tests/splitter.rs index e21404d66..4632a4808 100644 --- a/tests-integration/tests/splitter.rs +++ b/tests-integration/tests/splitter.rs @@ -2,16 +2,29 @@ use andromeda_app::app::AppComponent; use andromeda_app_contract::mock::{mock_andromeda_app, MockAppContract}; use andromeda_cw20::mock::{mock_andromeda_cw20, mock_cw20_instantiate_msg, mock_minter, MockCW20}; +use andromeda_kernel::KernelContract; use andromeda_testing::{ mock::{mock_app, MockApp}, mock_builder::MockAndromedaBuilder, MockAndromeda, MockContract, }; -use andromeda_std::amp::Recipient; -use cosmwasm_std::{coin, to_json_binary, Coin, Decimal, Empty, Uint128}; +use andromeda_std::{ + amp::{AndrAddr, Recipient}, + os::{ + self, + kernel::{AcknowledgementMsg, ExecuteMsg, InstantiateMsg, SendMessageWithFundsResponse}, + }, +}; +use cosmwasm_std::{ + coin, to_json_binary, Binary, Coin, Decimal, Empty, IbcAcknowledgement, IbcEndpoint, IbcPacket, + IbcPacketAckMsg, IbcTimeout, Timestamp, Uint128, +}; -use andromeda_finance::splitter::{AddressPercent, Cw20HookMsg}; +use andromeda_finance::splitter::{ + AddressPercent, Cw20HookMsg, ExecuteMsg as SplitterExecuteMsg, + InstantiateMsg as SplitterInstantiateMsg, +}; use andromeda_splitter::mock::{ mock_andromeda_splitter, mock_splitter_instantiate_msg, MockSplitter, }; @@ -300,3 +313,420 @@ fn test_successful_set_amount_splitter_cw20_with_remainder(setup: TestCase) { let cw20_balance = cw20.query_balance(&router, owner); assert_eq!(cw20_balance, Uint128::from(1_000_000u128 - 200u128)); } + +// Cross chain test +use andromeda_adodb::ADODBContract; +use andromeda_economics::EconomicsContract; +use andromeda_splitter::SplitterContract; +use andromeda_vfs::VFSContract; +use cw_orch::prelude::*; +use cw_orch_interchain::{prelude::*, types::IbcPacketOutcome, InterchainEnv}; +use ibc_relayer_types::core::ics24_host::identifier::PortId; +#[test] +fn test_splitter_cross_chain_recipient() { + // Here `juno-1` is the chain-id and `juno` is the address prefix for this chain + let sender = Addr::unchecked("sender_for_all_chains").into_string(); + let buyer = Addr::unchecked("buyer").into_string(); + + let interchain = MockInterchainEnv::new(vec![ + ("juno", &sender), + ("osmosis", &sender), + // Dummy chain to create unequal ports to test counterparty denom properly + ("cosmoshub", &sender), + ]); + + let juno = interchain.get_chain("juno").unwrap(); + let osmosis = interchain.get_chain("osmosis").unwrap(); + juno.set_balance(sender.clone(), vec![Coin::new(100000000000000, "juno")]) + .unwrap(); + juno.set_balance(buyer.clone(), vec![Coin::new(100000000000000, "juno")]) + .unwrap(); + + let kernel_juno = KernelContract::new(juno.clone()); + let vfs_juno = VFSContract::new(juno.clone()); + let adodb_juno = ADODBContract::new(juno.clone()); + let economics_juno = EconomicsContract::new(juno.clone()); + let splitter_juno = SplitterContract::new(juno.clone()); + + let kernel_osmosis = KernelContract::new(osmosis.clone()); + let vfs_osmosis = VFSContract::new(osmosis.clone()); + let adodb_osmosis = ADODBContract::new(osmosis.clone()); + + kernel_juno.upload().unwrap(); + vfs_juno.upload().unwrap(); + adodb_juno.upload().unwrap(); + economics_juno.upload().unwrap(); + splitter_juno.upload().unwrap(); + + kernel_osmosis.upload().unwrap(); + vfs_osmosis.upload().unwrap(); + adodb_osmosis.upload().unwrap(); + + let init_msg_juno = &InstantiateMsg { + owner: None, + chain_name: "juno".to_string(), + }; + let init_msg_osmosis = &InstantiateMsg { + owner: None, + chain_name: "osmosis".to_string(), + }; + + kernel_juno.instantiate(init_msg_juno, None, None).unwrap(); + kernel_osmosis + .instantiate(init_msg_osmosis, None, None) + .unwrap(); + + // Set up channel from juno to osmosis + let channel_receipt = interchain + .create_contract_channel(&kernel_juno, &kernel_osmosis, "andr-kernel-1", None) + .unwrap(); + + // After channel creation is complete, we get the channel id, which is necessary for ICA remote execution + let juno_channel = channel_receipt + .interchain_channel + .get_chain("juno") + .unwrap() + .channel + .unwrap(); + + // Set up channel from osmosis to cosmoshub for ICS20 transfers so that channel-0 is used on osmosis + // Later when we create channel with juno, channel-1 will be used on osmosis + let _channel_receipt = interchain + .create_channel( + "osmosis", + "cosmoshub", + &PortId::transfer(), + &PortId::transfer(), + "ics20-1", + None, + ) + .unwrap(); + + // Set up channel from juno to osmosis for ICS20 transfers + let channel_receipt = interchain + .create_channel( + "juno", + "osmosis", + &PortId::transfer(), + &PortId::transfer(), + "ics20-1", + None, + ) + .unwrap(); + + let channel = channel_receipt + .interchain_channel + .get_ordered_ports_from("juno") + .unwrap(); + + // After channel creation is complete, we get the channel id, which is necessary for ICA remote execution + let _juno_channel_ics20 = channel_receipt + .interchain_channel + .get_chain("juno") + .unwrap() + .channel + .unwrap(); + + vfs_juno + .instantiate( + &os::vfs::InstantiateMsg { + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + vfs_osmosis + .instantiate( + &os::vfs::InstantiateMsg { + kernel_address: kernel_osmosis.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + adodb_juno + .instantiate( + &os::adodb::InstantiateMsg { + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + adodb_juno + .execute( + &os::adodb::ExecuteMsg::Publish { + code_id: 4, + ado_type: "economics".to_string(), + action_fees: None, + version: "1.1.1".to_string(), + publisher: None, + }, + None, + ) + .unwrap(); + + adodb_juno + .execute( + &os::adodb::ExecuteMsg::Publish { + code_id: splitter_juno.code_id().unwrap(), + ado_type: "splitter".to_string(), + action_fees: None, + version: "2.3.0-b.1".to_string(), + publisher: None, + }, + None, + ) + .unwrap(); + + economics_juno + .instantiate( + &os::economics::InstantiateMsg { + kernel_address: kernel_juno.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + kernel_juno + .execute( + &ExecuteMsg::UpsertKeyAddress { + key: "economics".to_string(), + value: economics_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + + adodb_osmosis + .instantiate( + &os::adodb::InstantiateMsg { + kernel_address: kernel_osmosis.address().unwrap().into_string(), + owner: None, + }, + None, + None, + ) + .unwrap(); + + adodb_osmosis + .execute( + &os::adodb::ExecuteMsg::Publish { + code_id: 2, + ado_type: "counter".to_string(), + action_fees: None, + version: "1.0.2".to_string(), + publisher: None, + }, + None, + ) + .unwrap(); + + adodb_osmosis + .execute( + &os::adodb::ExecuteMsg::Publish { + code_id: 6, + ado_type: "economics".to_string(), + action_fees: None, + version: "1.1.1".to_string(), + publisher: None, + }, + None, + ) + .unwrap(); + + kernel_juno + .execute( + &ExecuteMsg::UpsertKeyAddress { + key: "vfs".to_string(), + value: vfs_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + + kernel_juno + .execute( + &ExecuteMsg::UpsertKeyAddress { + key: "adodb".to_string(), + value: adodb_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + + kernel_osmosis + .execute( + &ExecuteMsg::UpsertKeyAddress { + key: "vfs".to_string(), + value: vfs_osmosis.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + + kernel_osmosis + .execute( + &ExecuteMsg::UpsertKeyAddress { + key: "adodb".to_string(), + value: adodb_osmosis.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + + kernel_juno + .execute( + &ExecuteMsg::AssignChannels { + ics20_channel_id: Some(channel.clone().0.channel.unwrap().to_string()), + direct_channel_id: Some(juno_channel.to_string()), + chain: "osmosis".to_string(), + kernel_address: kernel_osmosis.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + + kernel_osmosis + .execute( + &ExecuteMsg::AssignChannels { + ics20_channel_id: Some(channel.0.channel.unwrap().to_string()), + direct_channel_id: Some(juno_channel.to_string()), + chain: "juno".to_string(), + kernel_address: kernel_juno.address().unwrap().into_string(), + }, + None, + ) + .unwrap(); + + let recipient = "osmo1qzskhrca90qy2yjjxqzq4yajy842x7c50xq33d"; + println!( + "osmosis kernel address: {}", + kernel_osmosis.address().unwrap() + ); + + splitter_juno + .instantiate( + &SplitterInstantiateMsg { + recipients: vec![AddressPercent { + recipient: Recipient { + address: AndrAddr::from_string(format!("ibc://osmosis/{}", recipient)), + msg: None, + ibc_recovery_address: None, + }, + percent: Decimal::one(), + }], + lock_time: None, + kernel_address: kernel_osmosis.address().unwrap().into_string(), + owner: None, + default_recipient: None, + }, + None, + None, + ) + .unwrap(); + + // Send funds to splitter + let splitter_juno_send_request = splitter_juno + .execute( + &SplitterExecuteMsg::Send { config: None }, + Some(&[Coin { + denom: "juno".to_string(), + amount: Uint128::new(100), + }]), + ) + .unwrap(); + + let packet_lifetime = interchain + .await_packets("juno", splitter_juno_send_request) + .unwrap(); + + let ibc_denom = format!("ibc/{}/{}", channel.1.channel.unwrap().as_str(), "juno"); + + // For testing a successful outcome of the first packet sent out in the tx, you can use: + if let IbcPacketOutcome::Success { .. } = &packet_lifetime.packets[0].outcome { + // Packet has been successfully acknowledged and decoded, the transaction has gone through correctly + // Check recipient balance + let balances = osmosis + .query_all_balances(kernel_osmosis.address().unwrap()) + .unwrap(); + assert_eq!(balances.len(), 1); + assert_eq!(balances[0].denom, ibc_denom); + assert_eq!(balances[0].amount.u128(), 100); + } else { + panic!("packet timed out"); + // There was a decode error or the packet timed out + // Else the packet timed-out, you may have a relayer error or something is wrong in your application + }; + + // Register trigger address + kernel_juno + .execute( + &ExecuteMsg::UpsertKeyAddress { + key: "trigger_key".to_string(), + value: sender.clone(), + }, + None, + ) + .unwrap(); + + // Construct an Execute msg from the kernel on juno inteded for the splitter on osmosis + let kernel_juno_trigger_request = kernel_juno + .execute( + &ExecuteMsg::TriggerRelay { + packet_sequence: "1".to_string(), + packet_ack_msg: IbcPacketAckMsg::new( + IbcAcknowledgement::new( + to_json_binary(&AcknowledgementMsg::::Ok( + SendMessageWithFundsResponse {}, + )) + .unwrap(), + ), + IbcPacket::new( + Binary::default(), + IbcEndpoint { + port_id: "port_id".to_string(), + channel_id: "channel_id".to_string(), + }, + IbcEndpoint { + port_id: "port_id".to_string(), + channel_id: "channel_id".to_string(), + }, + 1, + IbcTimeout::with_timestamp(Timestamp::from_seconds(1)), + ), + Addr::unchecked("relayer"), + ), + }, + None, + ) + .unwrap(); + + let packet_lifetime = interchain + .await_packets("juno", kernel_juno_trigger_request) + .unwrap(); + + // For testing a successful outcome of the first packet sent out in the tx, you can use: + if let IbcPacketOutcome::Success { .. } = &packet_lifetime.packets[0].outcome { + // Packet has been successfully acknowledged and decoded, the transaction has gone through correctly + + // Check recipient balance after trigger execute msg + let balances = osmosis.query_all_balances(recipient).unwrap(); + assert_eq!(balances.len(), 1); + assert_eq!(balances[0].denom, ibc_denom); + assert_eq!(balances[0].amount.u128(), 100); + } else { + panic!("packet timed out"); + // There was a decode error or the packet timed out + // Else the packet timed-out, you may have a relayer error or something is wrong in your application + }; +}