diff --git a/Cargo.lock b/Cargo.lock index d88c624ba..bc8b95278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1430,8 +1430,8 @@ dependencies = [ [[package]] name = "dapi-grpc" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "dapi-grpc-macros", "futures-core", @@ -1448,8 +1448,8 @@ dependencies = [ [[package]] name = "dapi-grpc-macros" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "heck", "quote", @@ -1545,8 +1545,8 @@ dependencies = [ [[package]] name = "dash-sdk" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "arc-swap", "async-trait", @@ -1657,8 +1657,8 @@ dependencies = [ [[package]] name = "dashpay-contract" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "platform-value", "platform-version", @@ -1668,8 +1668,8 @@ dependencies = [ [[package]] name = "data-contracts" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "dashpay-contract", "dpns-contract", @@ -1929,8 +1929,8 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] name = "dpns-contract" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "platform-value", "platform-version", @@ -1940,8 +1940,8 @@ dependencies = [ [[package]] name = "dpp" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "anyhow", "async-trait", @@ -1982,8 +1982,8 @@ dependencies = [ [[package]] name = "drive" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "bincode", "byteorder", @@ -2007,8 +2007,8 @@ dependencies = [ [[package]] name = "drive-proof-verifier" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "bincode", "dapi-grpc", @@ -2516,8 +2516,8 @@ dependencies = [ [[package]] name = "feature-flags-contract" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "platform-value", "platform-version", @@ -3738,8 +3738,8 @@ dependencies = [ [[package]] name = "keyword-search-contract" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "platform-value", "platform-version", @@ -3874,8 +3874,8 @@ dependencies = [ [[package]] name = "masternode-reward-shares-contract" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "platform-value", "platform-version", @@ -4904,8 +4904,8 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "platform-serialization" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "bincode", "platform-version", @@ -4913,8 +4913,8 @@ dependencies = [ [[package]] name = "platform-serialization-derive" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "proc-macro2", "quote", @@ -4924,8 +4924,8 @@ dependencies = [ [[package]] name = "platform-value" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "base64 0.22.1", "bincode", @@ -4944,8 +4944,8 @@ dependencies = [ [[package]] name = "platform-version" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "bincode", "grovedb-version", @@ -4956,8 +4956,8 @@ dependencies = [ [[package]] name = "platform-versioning" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "proc-macro2", "quote", @@ -5490,8 +5490,8 @@ dependencies = [ [[package]] name = "rs-dapi-client" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "backon", "chrono", @@ -6505,8 +6505,8 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "token-history-contract" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "platform-value", "platform-version", @@ -7072,8 +7072,8 @@ dependencies = [ [[package]] name = "wallet-utils-contract" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "platform-value", "platform-version", @@ -8017,8 +8017,8 @@ dependencies = [ [[package]] name = "withdrawals-contract" -version = "2.0.0-rc.10" -source = "git+https://github.com/dashpay/platform.git?rev=64f0c69d013df26e58b59594c55f7690b9bc358c#64f0c69d013df26e58b59594c55f7690b9bc358c" +version = "2.0.0-rc.11" +source = "git+https://github.com/dashpay/platform.git?rev=9c98e6b72cd250dd7ea9ded1d74fe84d17733915#9c98e6b72cd250dd7ea9ded1d74fe84d17733915" dependencies = [ "num_enum 0.5.11", "platform-value", diff --git a/Cargo.toml b/Cargo.toml index ba81d4b7b..76ee7ae2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ rfd = "0.15.1" qrcode = "0.14.1" eframe = { version = "0.31.1", features = ["persistence"] } base64 = "0.22.1" -dash-sdk = { git = "https://github.com/dashpay/platform.git", rev = "64f0c69d013df26e58b59594c55f7690b9bc358c" } +dash-sdk = { git = "https://github.com/dashpay/platform.git", rev = "9c98e6b72cd250dd7ea9ded1d74fe84d17733915" } thiserror = "2" serde = "1.0.219" serde_json = "1.0.140" diff --git a/src/backend_task/tokens/burn_tokens.rs b/src/backend_task/tokens/burn_tokens.rs index 794b28071..f3fbc34b1 100644 --- a/src/backend_task/tokens/burn_tokens.rs +++ b/src/backend_task/tokens/burn_tokens.rs @@ -4,10 +4,9 @@ use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::proof_log_item::{ProofLogItem, RequestType}; +use dash_sdk::dpp::group::GroupStateTransitionInfoStatus; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; -use dash_sdk::dpp::state_transition::StateTransitionSigningOptions; use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; use dash_sdk::platform::transition::fungible_tokens::burn::TokenBurnTransitionBuilder; use dash_sdk::platform::{DataContract, IdentityPublicKey}; @@ -23,6 +22,7 @@ impl AppContext { signing_key: IdentityPublicKey, public_note: Option, amount: u64, + group_info: Option, sdk: &Sdk, _sender: mpsc::Sender, ) -> Result { @@ -37,6 +37,10 @@ impl AppContext { builder = builder.with_public_note(note); } + if let Some(group_info) = group_info { + builder = builder.with_using_group_info(group_info); + } + let options = self.state_transition_options(); let state_transition = builder diff --git a/src/backend_task/tokens/claim_tokens.rs b/src/backend_task/tokens/claim_tokens.rs index 53c22c71c..b3bcc0647 100644 --- a/src/backend_task/tokens/claim_tokens.rs +++ b/src/backend_task/tokens/claim_tokens.rs @@ -5,9 +5,7 @@ use crate::model::qualified_identity::QualifiedIdentity; use crate::model::proof_log_item::{ProofLogItem, RequestType}; use dash_sdk::dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; -use dash_sdk::dpp::state_transition::StateTransitionSigningOptions; use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; use dash_sdk::platform::transition::fungible_tokens::claim::TokenClaimTransitionBuilder; use dash_sdk::platform::{DataContract, IdentityPublicKey}; diff --git a/src/backend_task/tokens/destroy_frozen_funds.rs b/src/backend_task/tokens/destroy_frozen_funds.rs index 436239153..67f0cc3ba 100644 --- a/src/backend_task/tokens/destroy_frozen_funds.rs +++ b/src/backend_task/tokens/destroy_frozen_funds.rs @@ -4,10 +4,9 @@ use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::proof_log_item::{ProofLogItem, RequestType}; +use dash_sdk::dpp::group::GroupStateTransitionInfoStatus; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; -use dash_sdk::dpp::state_transition::StateTransitionSigningOptions; use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; use dash_sdk::platform::transition::fungible_tokens::destroy::TokenDestroyFrozenFundsTransitionBuilder; use dash_sdk::platform::{DataContract, Identifier, IdentityPublicKey}; @@ -23,6 +22,7 @@ impl AppContext { signing_key: IdentityPublicKey, public_note: Option, frozen_identity: Identifier, + group_info: Option, sdk: &Sdk, _sender: mpsc::Sender, ) -> Result { @@ -37,6 +37,10 @@ impl AppContext { builder = builder.with_public_note(note); } + if let Some(group_info) = group_info { + builder = builder.with_using_group_info(group_info); + } + let options = self.state_transition_options(); let state_transition = builder diff --git a/src/backend_task/tokens/freeze_tokens.rs b/src/backend_task/tokens/freeze_tokens.rs index 1898097b9..3d187a5d0 100644 --- a/src/backend_task/tokens/freeze_tokens.rs +++ b/src/backend_task/tokens/freeze_tokens.rs @@ -4,10 +4,9 @@ use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::proof_log_item::{ProofLogItem, RequestType}; +use dash_sdk::dpp::group::GroupStateTransitionInfoStatus; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; -use dash_sdk::dpp::state_transition::StateTransitionSigningOptions; use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; use dash_sdk::platform::transition::fungible_tokens::freeze::TokenFreezeTransitionBuilder; use dash_sdk::platform::{DataContract, Identifier, IdentityPublicKey}; @@ -23,6 +22,7 @@ impl AppContext { signing_key: IdentityPublicKey, public_note: Option, freeze_identity: Identifier, + group_info: Option, sdk: &Sdk, _sender: mpsc::Sender, ) -> Result { @@ -37,6 +37,10 @@ impl AppContext { builder = builder.with_public_note(note); } + if let Some(group_info) = group_info { + builder = builder.with_using_group_info(group_info); + } + let options = self.state_transition_options(); let state_transition = builder diff --git a/src/backend_task/tokens/mod.rs b/src/backend_task/tokens/mod.rs index 38207b386..9ec7e2d1d 100644 --- a/src/backend_task/tokens/mod.rs +++ b/src/backend_task/tokens/mod.rs @@ -1,5 +1,5 @@ use super::BackendTaskSuccessResult; -use crate::ui::tokens::tokens_screen::{IdentityTokenBalance, IdentityTokenIdentifier, TokenInfo}; +use crate::ui::tokens::tokens_screen::{IdentityTokenIdentifier, IdentityTokenInfo, TokenInfo}; use crate::{app::TaskResult, context::AppContext, model::qualified_identity::QualifiedIdentity}; use dash_sdk::dpp::balances::credits::TokenAmount; use dash_sdk::dpp::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; @@ -121,6 +121,7 @@ pub(crate) enum TokenTask { signing_key: IdentityPublicKey, public_note: Option, amount: TokenAmount, + group_info: Option, }, DestroyFrozenFunds { actor_identity: QualifiedIdentity, @@ -129,6 +130,7 @@ pub(crate) enum TokenTask { signing_key: IdentityPublicKey, public_note: Option, frozen_identity: Identifier, + group_info: Option, }, FreezeTokens { actor_identity: QualifiedIdentity, @@ -137,6 +139,7 @@ pub(crate) enum TokenTask { signing_key: IdentityPublicKey, public_note: Option, freeze_identity: Identifier, + group_info: Option, }, UnfreezeTokens { actor_identity: QualifiedIdentity, @@ -145,6 +148,7 @@ pub(crate) enum TokenTask { signing_key: IdentityPublicKey, public_note: Option, unfreeze_identity: Identifier, + group_info: Option, }, PauseTokens { actor_identity: QualifiedIdentity, @@ -152,6 +156,7 @@ pub(crate) enum TokenTask { token_position: TokenContractPosition, signing_key: IdentityPublicKey, public_note: Option, + group_info: Option, }, ResumeTokens { actor_identity: QualifiedIdentity, @@ -159,6 +164,7 @@ pub(crate) enum TokenTask { token_position: TokenContractPosition, signing_key: IdentityPublicKey, public_note: Option, + group_info: Option, }, ClaimTokens { data_contract: DataContract, @@ -173,10 +179,11 @@ pub(crate) enum TokenTask { token_id: Identifier, }, UpdateTokenConfig { - identity_token_balance: IdentityTokenBalance, + identity_token_info: IdentityTokenInfo, change_item: TokenConfigurationChangeItem, signing_key: IdentityPublicKey, public_note: Option, + group_info: Option, }, PurchaseTokens { identity: QualifiedIdentity, @@ -335,6 +342,7 @@ impl AppContext { signing_key, public_note, amount, + group_info, } => self .burn_tokens( owner_identity, @@ -343,6 +351,7 @@ impl AppContext { signing_key.clone(), public_note.clone(), *amount, + group_info.clone(), sdk, sender, ) @@ -355,6 +364,7 @@ impl AppContext { signing_key, public_note, frozen_identity, + group_info, } => self .destroy_frozen_funds( actor_identity, @@ -363,6 +373,7 @@ impl AppContext { signing_key.clone(), public_note.clone(), frozen_identity.clone(), + group_info.clone(), sdk, sender, ) @@ -375,6 +386,7 @@ impl AppContext { signing_key, public_note, freeze_identity, + group_info, } => self .freeze_tokens( actor_identity, @@ -383,6 +395,7 @@ impl AppContext { signing_key.clone(), public_note.clone(), freeze_identity.clone(), + group_info.clone(), sdk, sender, ) @@ -395,6 +408,7 @@ impl AppContext { signing_key, public_note, unfreeze_identity, + group_info, } => self .unfreeze_tokens( actor_identity, @@ -403,6 +417,7 @@ impl AppContext { signing_key.clone(), public_note.clone(), unfreeze_identity.clone(), + group_info.clone(), sdk, sender, ) @@ -414,6 +429,7 @@ impl AppContext { token_position, signing_key, public_note, + group_info, } => self .pause_tokens( actor_identity, @@ -421,6 +437,7 @@ impl AppContext { *token_position, signing_key.clone(), public_note.clone(), + group_info.clone(), sdk, sender, ) @@ -432,6 +449,7 @@ impl AppContext { token_position, signing_key, public_note, + group_info, } => self .resume_tokens( actor_identity, @@ -439,6 +457,7 @@ impl AppContext { *token_position, signing_key.clone(), public_note.clone(), + group_info.clone(), sdk, sender, ) @@ -517,16 +536,18 @@ impl AppContext { )) } TokenTask::UpdateTokenConfig { - identity_token_balance, + identity_token_info, change_item, signing_key, public_note, + group_info, } => self .update_token_config( - identity_token_balance.clone(), + identity_token_info.clone(), change_item.clone(), signing_key, public_note.clone(), + group_info.clone(), sdk, ) .await diff --git a/src/backend_task/tokens/pause_tokens.rs b/src/backend_task/tokens/pause_tokens.rs index 756f4d566..1210e508b 100644 --- a/src/backend_task/tokens/pause_tokens.rs +++ b/src/backend_task/tokens/pause_tokens.rs @@ -4,10 +4,9 @@ use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::proof_log_item::{ProofLogItem, RequestType}; +use dash_sdk::dpp::group::GroupStateTransitionInfoStatus; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; -use dash_sdk::dpp::state_transition::StateTransitionSigningOptions; use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; use dash_sdk::platform::transition::fungible_tokens::emergency_action::TokenEmergencyActionTransitionBuilder; use dash_sdk::platform::{DataContract, IdentityPublicKey}; @@ -22,6 +21,7 @@ impl AppContext { token_position: u16, signing_key: IdentityPublicKey, public_note: Option, + group_info: Option, sdk: &Sdk, _sender: mpsc::Sender, ) -> Result { @@ -36,6 +36,10 @@ impl AppContext { builder = builder.with_public_note(note); } + if let Some(group_info) = group_info { + builder = builder.with_using_group_info(group_info); + } + let options = self.state_transition_options(); let state_transition = builder diff --git a/src/backend_task/tokens/resume_tokens.rs b/src/backend_task/tokens/resume_tokens.rs index 2cda2fc3e..a05465b10 100644 --- a/src/backend_task/tokens/resume_tokens.rs +++ b/src/backend_task/tokens/resume_tokens.rs @@ -4,10 +4,9 @@ use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::proof_log_item::{ProofLogItem, RequestType}; +use dash_sdk::dpp::group::GroupStateTransitionInfoStatus; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; -use dash_sdk::dpp::state_transition::StateTransitionSigningOptions; use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; use dash_sdk::platform::transition::fungible_tokens::emergency_action::TokenEmergencyActionTransitionBuilder; use dash_sdk::platform::{DataContract, IdentityPublicKey}; @@ -22,6 +21,7 @@ impl AppContext { token_position: u16, signing_key: IdentityPublicKey, public_note: Option, + group_info: Option, sdk: &Sdk, _sender: mpsc::Sender, ) -> Result { @@ -36,6 +36,10 @@ impl AppContext { builder = builder.with_public_note(note); } + if let Some(group_info) = group_info { + builder = builder.with_using_group_info(group_info); + } + let options = self.state_transition_options(); let state_transition = builder diff --git a/src/backend_task/tokens/transfer_tokens.rs b/src/backend_task/tokens/transfer_tokens.rs index 5c518c060..1577f19a1 100644 --- a/src/backend_task/tokens/transfer_tokens.rs +++ b/src/backend_task/tokens/transfer_tokens.rs @@ -4,9 +4,7 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; -use dash_sdk::dpp::state_transition::StateTransitionSigningOptions; use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; use dash_sdk::platform::transition::fungible_tokens::transfer::TokenTransferTransitionBuilder; use dash_sdk::platform::{DataContract, Identifier, IdentityPublicKey}; diff --git a/src/backend_task/tokens/unfreeze_tokens.rs b/src/backend_task/tokens/unfreeze_tokens.rs index 5bf6cc4fc..ac9d02f1d 100644 --- a/src/backend_task/tokens/unfreeze_tokens.rs +++ b/src/backend_task/tokens/unfreeze_tokens.rs @@ -4,10 +4,9 @@ use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::proof_log_item::{ProofLogItem, RequestType}; +use dash_sdk::dpp::group::GroupStateTransitionInfoStatus; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; -use dash_sdk::dpp::state_transition::StateTransitionSigningOptions; use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; use dash_sdk::platform::transition::fungible_tokens::unfreeze::TokenUnfreezeTransitionBuilder; use dash_sdk::platform::{DataContract, Identifier, IdentityPublicKey}; @@ -23,6 +22,7 @@ impl AppContext { signing_key: IdentityPublicKey, public_note: Option, unfreeze_identity: Identifier, + group_info: Option, sdk: &Sdk, _sender: mpsc::Sender, ) -> Result { @@ -37,6 +37,10 @@ impl AppContext { builder = builder.with_public_note(note); } + if let Some(group_info) = group_info { + builder = builder.with_using_group_info(group_info); + } + let options = self.state_transition_options(); let state_transition = builder diff --git a/src/backend_task/tokens/update_token_config.rs b/src/backend_task/tokens/update_token_config.rs index 11ebeeb3f..2dbb46bfd 100644 --- a/src/backend_task/tokens/update_token_config.rs +++ b/src/backend_task/tokens/update_token_config.rs @@ -1,8 +1,11 @@ use super::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::proof_log_item::{ProofLogItem, RequestType}; -use crate::ui::tokens::tokens_screen::IdentityTokenBalance; +use crate::ui::tokens::tokens_screen::IdentityTokenInfo; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::group::GroupStateTransitionInfoStatus; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; use dash_sdk::platform::{DataContract, Fetch, IdentityPublicKey}; @@ -15,51 +18,54 @@ use dash_sdk::{ impl AppContext { pub async fn update_token_config( &self, - identity_token_balance: IdentityTokenBalance, + identity_token_info: IdentityTokenInfo, change_item: TokenConfigurationChangeItem, signing_key: &IdentityPublicKey, public_note: Option, + group_info: Option, sdk: &Sdk, ) -> Result { // Get the existing contract and identity for building the state transition // First, fetch the contract from the local database let existing_data_contract = &self - .get_contract_by_id(&identity_token_balance.data_contract_id) + .get_contract_by_id(&identity_token_info.data_contract.contract.id()) .map_err(|e| { format!( "Error getting contract by ID {}: {}", - identity_token_balance.data_contract_id, e + identity_token_info.data_contract.contract.id(), + e ) })? .ok_or_else(|| { format!( "Contract with ID {} not found", - identity_token_balance.data_contract_id + identity_token_info.data_contract.contract.id() ) })? .contract; // Then, fetch the identity from the local database let identity = self - .get_identity_by_id(&identity_token_balance.identity_id) + .get_identity_by_id(&identity_token_info.identity.identity.id()) .map_err(|e| { format!( "Error getting identity by ID {}: {}", - identity_token_balance.identity_id, e + identity_token_info.identity.identity.id(), + e ) })? .ok_or_else(|| { format!( "Identity with ID {} not found", - identity_token_balance.identity_id + identity_token_info.identity.identity.id() ) })?; // Create the TokenConfigUpdateTransition let mut builder = TokenConfigUpdateTransitionBuilder::new( existing_data_contract, - identity_token_balance.token_position, - identity_token_balance.identity_id, + identity_token_info.token_position, + identity_token_info.identity.identity.id(), change_item.clone(), ); @@ -68,6 +74,10 @@ impl AppContext { builder = builder.with_public_note(public_note.clone()); } + if let Some(group_info) = group_info { + builder = builder.with_using_group_info(group_info); + } + let options = self.state_transition_options(); // Sign the state transition @@ -106,38 +116,37 @@ impl AppContext { e => format!("Error broadcasting Update token config transition: {}", e), })?; - // Now update the token in the local database - // First, fetch the updated contract from Platform - let data_contract = DataContract::fetch(sdk, identity_token_balance.data_contract_id) - .await - .map_err(|e| format!("Error fetching contract from platform: {}", e.to_string()))? - .ok_or_else(|| { - format!( - "Contract with ID {} not found on platform", - identity_token_balance.data_contract_id - ) - })?; + // Now update the data contract in the local database + // First, fetch the updated contract from the platform + let data_contract = + DataContract::fetch(sdk, identity_token_info.data_contract.contract.id()) + .await + .map_err(|e| format!("Error fetching contract from platform: {}", e.to_string()))? + .ok_or_else(|| { + format!( + "Contract with ID {} not found on platform", + identity_token_info.data_contract.contract.id() + ) + })?; let token = data_contract .tokens() - .get(&identity_token_balance.token_position) + .get(&identity_token_info.token_position) .ok_or_else(|| { format!( "Token with position {} not found in contract", - identity_token_balance.token_position + identity_token_info.token_position ) })?; // Then replace the contract in the local database - self.replace_contract(identity_token_balance.data_contract_id, &data_contract) - .map_err(|e| { - format!( - "Error replacing contract in local database: {}", - e.to_string() - ) - })?; + self.replace_contract( + identity_token_info.data_contract.contract.id(), + &data_contract, + ) + .map_err(|e| format!("Error replacing contract in local database: {}", e))?; - self.remove_token(&identity_token_balance.token_id) + self.remove_token(&identity_token_info.token_id) .map_err(|e| { format!( "Error removing token from local database: {}", @@ -146,11 +155,11 @@ impl AppContext { })?; self.insert_token( - &identity_token_balance.token_id, - &identity_token_balance.token_alias, + &identity_token_info.token_id, + &identity_token_info.token_alias, token.clone(), - &identity_token_balance.data_contract_id, - identity_token_balance.token_position, + &identity_token_info.data_contract.contract.id(), + identity_token_info.token_position, ) .map_err(|e| { format!( diff --git a/src/database/tokens.rs b/src/database/tokens.rs index c742c90be..896eefb4b 100644 --- a/src/database/tokens.rs +++ b/src/database/tokens.rs @@ -1,6 +1,7 @@ use bincode::{self, config::standard}; use dash_sdk::dpp::dashcore::Network; use dash_sdk::dpp::data_contract::TokenConfiguration; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::platform::Identifier; use dash_sdk::query_types::IndexMap; use rusqlite::params; @@ -122,12 +123,17 @@ impl Database { ], )?; + // Insert an identity token balance of 0 for each identity for this token + let wallets = app_context.wallets.read().unwrap(); + for identity in self.get_local_qualified_identities(app_context, &wallets)? { + self.insert_identity_token_balance(&token_id, &identity.identity.id(), 0, app_context)?; + } + Ok(()) } - /// Creates the identity_token_balances table if it doesn't already exist + /// Drops the identity_token_balances table (if necessary to enforce schema update) pub fn drop_identity_token_balances_table(&self) -> rusqlite::Result<()> { - // Drop existing token table (if necessary to enforce schema update) self.execute("DROP TABLE IF EXISTS identity_token_balances", [])?; Ok(()) @@ -172,6 +178,7 @@ impl Database { Ok(()) } + /// Retrieves all known tokens as a map from token ID to `TokenInfo`. /// /// Now also fetches and decodes the **`token_config`** blob. diff --git a/src/ui/contracts_documents/group_actions_screen.rs b/src/ui/contracts_documents/group_actions_screen.rs index 0b410bae0..97f06a6bf 100644 --- a/src/ui/contracts_documents/group_actions_screen.rs +++ b/src/ui/contracts_documents/group_actions_screen.rs @@ -18,7 +18,18 @@ use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::helpers::add_contract_chooser_pre_filtered; use crate::ui::helpers::render_identity_selector; -use crate::ui::{MessageType, RootScreenType, ScreenLike}; +use crate::ui::tokens::burn_tokens_screen::BurnTokensScreen; +use crate::ui::tokens::destroy_frozen_funds_screen::DestroyFrozenFundsScreen; +use crate::ui::tokens::freeze_tokens_screen::FreezeTokensScreen; +use crate::ui::tokens::mint_tokens_screen::MintTokensScreen; +use crate::ui::tokens::pause_tokens_screen::PauseTokensScreen; +use crate::ui::tokens::resume_tokens_screen::ResumeTokensScreen; +use crate::ui::tokens::tokens_screen::{ + IdentityTokenBalance, IdentityTokenIdentifier, IdentityTokenInfo, +}; +use crate::ui::tokens::unfreeze_tokens_screen::UnfreezeTokensScreen; +use crate::ui::tokens::update_token_config::UpdateTokenConfigScreen; +use crate::ui::{MessageType, RootScreenType, Screen, ScreenLike}; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; @@ -26,7 +37,8 @@ use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers use dash_sdk::dpp::data_contract::change_control_rules::ChangeControlRules; use dash_sdk::dpp::data_contract::TokenContractPosition; use dash_sdk::dpp::group::action_event::GroupActionEvent; -use dash_sdk::dpp::group::group_action::GroupAction; +use dash_sdk::dpp::group::group_action::{GroupAction, GroupActionAccessors}; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::dpp::prelude::TimestampMillis; use dash_sdk::dpp::tokens::token_event::TokenEvent; @@ -60,6 +72,7 @@ pub struct GroupActionsScreen { >, contract_search: String, qualified_identities: Vec, + identity_token_balances: IndexMap, selected_identity: Option, // Backend task status @@ -73,7 +86,10 @@ impl GroupActionsScreen { pub fn new(app_context: &Arc) -> Self { let qualified_identities = app_context .load_local_qualified_identities() - .expect("Failed to load identities"); + .unwrap_or_else(|_| { + tracing::info!("Failed to load local qualified identities"); + vec![] + }); let contracts_with_group_actions = app_context.db.get_contracts(app_context, None, None).unwrap_or_default().into_iter().filter_map(|qualified_contract| { let tokens = qualified_contract.contract.tokens().clone().into_iter().filter_map(|(pos, token_config)| { @@ -106,12 +122,21 @@ impl GroupActionsScreen { } }).collect(); + let identity_token_balances = match app_context.identity_token_balances() { + Ok(identity_token_balances) => identity_token_balances, + Err(e) => { + tracing::error!("Failed to load identity token balances: {}", e); + IndexMap::new() + } + }; + Self { // Contract and identity selectors selected_contract: None, contracts_with_group_actions, contract_search: String::new(), qualified_identities, + identity_token_balances, selected_identity: None, // Backend task status @@ -123,16 +148,18 @@ impl GroupActionsScreen { } fn render_group_actions( - &self, + &mut self, ui: &mut egui::Ui, group_actions: &IndexMap, ) -> AppAction { + let mut action = AppAction::None; + ui.heading("Active Group Actions:"); ui.add_space(10.0); if group_actions.is_empty() { ui.label("No active group actions found."); - return AppAction::None; + return action; } let text_style = TextStyle::Body; @@ -160,7 +187,7 @@ impl GroupActionsScreen { }); } header.col(|ui| { - ui.label(""); // No header for button + ui.label(""); }); }) .body(|mut body| { @@ -174,9 +201,9 @@ impl GroupActionsScreen { format!("{} to {}", amount, identifier), format!("{}", note_opt.clone().unwrap_or_default()), ), - TokenEvent::Burn(amount, note_opt) => ( + TokenEvent::Burn(amount, burn_from, note_opt) => ( "Burn", - format!("{}", amount), + format!("{} from {}", amount, burn_from), format!("{}", note_opt.clone().unwrap_or_default()), ), TokenEvent::Freeze(identifier, note_opt) => ( @@ -264,8 +291,9 @@ impl GroupActionsScreen { }); }); row.col(|ui| { + let info_clone = info.clone(); ui.horizontal(|ui| { - ui.label(info); + ui.label(info_clone); ui.add_space(30.0); }); }); @@ -286,7 +314,155 @@ impl GroupActionsScreen { ) .clicked() { - // return AppAction::GoToGroupActionScreen(id.clone()); + let token_contract_position = match group_action { + GroupAction::V0(action_v0) => action_v0 + .token_contract_position() + .clone(), + }; + let token_id = self.selected_contract.clone().expect("No contract selected").contract.token_id(token_contract_position).expect("No token ID found at the given position"); + let identity_token_balance = match self + .identity_token_balances + .get(&IdentityTokenIdentifier { + token_id, + identity_id: self.selected_identity + .as_ref() + .unwrap() + .identity + .id() + }) + .cloned() { + Some(identity_token_balance) => identity_token_balance, + None => { + self.fetch_group_actions_status = + FetchGroupActionsStatus::ErrorMessage( + "No identity token balance found".to_string(), + ); + return; + } + }; + let identity_token_info = match IdentityTokenInfo::try_from_identity_token_balance_with_lookup(&identity_token_balance, &self.app_context) { + Ok(identity_token_info) => identity_token_info, + Err(e) => { + self.fetch_group_actions_status = + FetchGroupActionsStatus::ErrorMessage( + format!("Failed to get identity token info: {}", e), + ); + return; + } + }; + match typ { + "Mint" => { + let mut mint_screen = + MintTokensScreen::new(identity_token_info, &self.app_context); + + mint_screen.group_action_id = Some(*id); + mint_screen.amount_to_mint = info + .split_whitespace() + .next() + .unwrap_or("0") + .to_string(); + + action |= AppAction::AddScreen( + Screen::MintTokensScreen(mint_screen), + ); + } + "Burn" => { + let mut burn_screen = + BurnTokensScreen::new(identity_token_info, &self.app_context); + + burn_screen.group_action_id = Some(*id); + burn_screen.amount_to_burn = info + .split_whitespace() + .next() + .unwrap_or("0") + .to_string(); + + action |= AppAction::AddScreen( + Screen::BurnTokensScreen(burn_screen), + ); + } + "Freeze" => { + let mut freeze_screen = FreezeTokensScreen::new( + identity_token_info, &self.app_context, + ); + freeze_screen.group_action_id = Some(*id); + freeze_screen.freeze_identity_id = info + .split_whitespace() + .next() + .unwrap_or("") + .to_string(); + action |= AppAction::AddScreen( + Screen::FreezeTokensScreen(freeze_screen), + ); + } + "Unfreeze" => { + let mut unfreeze_screen = UnfreezeTokensScreen::new( + identity_token_info, &self.app_context, + ); + unfreeze_screen.group_action_id = Some(*id); + unfreeze_screen.unfreeze_identity_id = info + .split_whitespace() + .next() + .unwrap_or("") + .to_string(); + action |= AppAction::AddScreen( + Screen::UnfreezeTokensScreen(unfreeze_screen), + ); + } + "DestroyFrozenFunds" => { + let mut destroy_screen = + DestroyFrozenFundsScreen::new( + identity_token_info, &self.app_context, + ); + destroy_screen.group_action_id = Some(*id); + destroy_screen.frozen_identity_id = info + .split_whitespace() + .nth(2) + .unwrap_or("") + .to_string(); + action |= AppAction::AddScreen( + Screen::DestroyFrozenFundsScreen( + destroy_screen, + ), + ); + } + "Emergency" => match info.split_whitespace().nth(1) { + Some("Pause") => { + let mut pause_screen = PauseTokensScreen::new( + identity_token_info, &self.app_context, + ); + pause_screen.group_action_id = Some(*id); + action |= AppAction::AddScreen( + Screen::PauseTokensScreen(pause_screen), + ); + } + Some("Resume") => { + let mut resume_screen = ResumeTokensScreen::new( + identity_token_info, &self.app_context, + ); + resume_screen.group_action_id = Some(*id); + action |= AppAction::AddScreen( + Screen::ResumeTokensScreen(resume_screen), + ); + } + _ => {} + }, + "ConfigUpdate" => { + let mut update_screen = + UpdateTokenConfigScreen::new( + identity_token_info, &self.app_context, + ); + update_screen.group_action_id = Some(*id); + action |= AppAction::AddScreen( + Screen::UpdateTokenConfigScreen(update_screen), + ); + } + + // To do: Change price and direct purchase + _ => { + action |= AppAction::None; + } + } } }); }); @@ -294,11 +470,15 @@ impl GroupActionsScreen { }); }); - AppAction::None + action } } impl ScreenLike for GroupActionsScreen { + fn refresh(&mut self) { + self.fetch_group_actions_status = FetchGroupActionsStatus::NotStarted; + } + fn display_message(&mut self, message: &str, message_type: MessageType) { match message_type { MessageType::Success => { @@ -457,10 +637,11 @@ impl ScreenLike for GroupActionsScreen { if let FetchGroupActionsStatus::Complete(group_actions) = &self.fetch_group_actions_status { + let group_actions = group_actions.clone(); ui.add_space(10.0); ui.separator(); ui.add_space(10.0); - action |= self.render_group_actions(ui, group_actions); + action |= self.render_group_actions(ui, &group_actions); } }); diff --git a/src/ui/helpers.rs b/src/ui/helpers.rs index 34bacd257..bdf490140 100644 --- a/src/ui/helpers.rs +++ b/src/ui/helpers.rs @@ -9,6 +9,7 @@ use dash_sdk::{ data_contract::{ accessors::v0::DataContractV0Getters, document_type::{accessors::DocumentTypeV0Getters, DocumentType}, + group::{accessors::v0::GroupV0Getters, Group}, }, identity::{ accessors::IdentityGettersV0, @@ -16,9 +17,11 @@ use dash_sdk::{ }, platform_value::string_encoding::Encoding, }, - platform::IdentityPublicKey, + platform::{Identifier, IdentityPublicKey}, }; -use egui::{ComboBox, Ui}; +use egui::{Color32, ComboBox, Ui}; + +use super::tokens::tokens_screen::IdentityTokenInfo; /// Returns the newly selected identity (if changed), otherwise the existing one. pub fn render_identity_selector( @@ -254,3 +257,83 @@ pub fn add_contract_chooser_pre_filtered<'a, T>( ui.end_row(); }); } + +pub fn render_group_action_text( + ui: &mut Ui, + group: &Option<(u16, Group)>, + identity_token_info: &IdentityTokenInfo, + group_action_type_str: &str, + group_action_id: &Option, +) -> String { + if let Some(group_action_id) = group_action_id { + ui.add_space(20.0); + ui.add(egui::Label::new( + egui::RichText::new("This is a group action.") + .heading() + .color(egui::Color32::DARK_RED), + )); + + ui.add_space(10.0); + ui.label(format!( + "You are signing an active {} group action (Action ID {})", + group_action_type_str, + group_action_id.to_string(Encoding::Base58) + )); + return format!("Sign {}", group_action_type_str); + } else if let Some((_, group)) = group.as_ref() { + let your_power = group + .members() + .get(&identity_token_info.identity.identity.id()); + + ui.add_space(20.0); + ui.add(egui::Label::new( + egui::RichText::new("This is a group action.") + .heading() + .color(egui::Color32::DARK_RED), + )); + + if your_power.is_none() { + ui.add_space(10.0); + ui.colored_label( + Color32::DARK_RED, + format!( + "You are not a valid group member for {} on this token", + group_action_type_str + ), + ); + return format!("Test {} (Should fail)", group_action_type_str); + } + + ui.add_space(10.0); + if let Some(your_power) = your_power { + if *your_power >= group.required_power() { + ui.label(format!("You are a unilateral group member.\nYou do not need other group members to sign off on this action for it to process.")); + format!("{}", group_action_type_str) + } else { + ui.label(format!("You are not a unilateral group member.\nYou can initiate the {group_action_type_str} action but will need other group members to sign off on it for it to process.\nThis action requires a total power of {}.\nYour power is {your_power}.", group.required_power())); + + ui.add_space(10.0); + ui.label(format!( + "Other group members are : \n{}", + group + .members() + .iter() + .filter_map(|(member, power)| { + if member != &identity_token_info.identity.identity.id() { + Some(format!(" - {} with power {}", member, power)) + } else { + None + } + }) + .collect::>() + .join(", \n") + )); + format!("Initiate Group {}", group_action_type_str) + } + } else { + format!("Test {} (It should fail)", group_action_type_str) + } + } else { + format!("{}", group_action_type_str) + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9f355e909..094cb6dd9 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -197,15 +197,15 @@ pub enum ScreenType { AddTokenById, TransferTokensScreen(IdentityTokenBalance), MintTokensScreen(IdentityTokenInfo), - BurnTokensScreen(IdentityTokenBalance), - DestroyFrozenFundsScreen(IdentityTokenBalance), - FreezeTokensScreen(IdentityTokenBalance), - UnfreezeTokensScreen(IdentityTokenBalance), - PauseTokensScreen(IdentityTokenBalance), - ResumeTokensScreen(IdentityTokenBalance), + BurnTokensScreen(IdentityTokenInfo), + DestroyFrozenFundsScreen(IdentityTokenInfo), + FreezeTokensScreen(IdentityTokenInfo), + UnfreezeTokensScreen(IdentityTokenInfo), + PauseTokensScreen(IdentityTokenInfo), + ResumeTokensScreen(IdentityTokenInfo), ClaimTokensScreen(IdentityTokenBalance), ViewTokenClaimsScreen(IdentityTokenBalance), - UpdateTokenConfigScreen(IdentityTokenBalance), + UpdateTokenConfigScreen(IdentityTokenInfo), PurchaseTokenScreen(IdentityTokenInfo), SetTokenPriceScreen(IdentityTokenInfo), } @@ -317,39 +317,27 @@ impl ScreenType { ScreenType::MintTokensScreen(identity_token_info) => Screen::MintTokensScreen( MintTokensScreen::new(identity_token_info.clone(), app_context), ), - ScreenType::BurnTokensScreen(identity_token_balance) => Screen::BurnTokensScreen( - BurnTokensScreen::new(identity_token_balance.clone(), app_context), + ScreenType::BurnTokensScreen(identity_token_info) => Screen::BurnTokensScreen( + BurnTokensScreen::new(identity_token_info.clone(), app_context), ), - ScreenType::DestroyFrozenFundsScreen(identity_token_balance) => { + ScreenType::DestroyFrozenFundsScreen(identity_token_info) => { Screen::DestroyFrozenFundsScreen(DestroyFrozenFundsScreen::new( - identity_token_balance.clone(), - app_context, - )) - } - ScreenType::FreezeTokensScreen(identity_token_balance) => { - Screen::DestroyFrozenFundsScreen(DestroyFrozenFundsScreen::new( - identity_token_balance.clone(), - app_context, - )) - } - ScreenType::UnfreezeTokensScreen(identity_token_balance) => { - Screen::DestroyFrozenFundsScreen(DestroyFrozenFundsScreen::new( - identity_token_balance.clone(), - app_context, - )) - } - ScreenType::PauseTokensScreen(identity_token_balance) => { - Screen::DestroyFrozenFundsScreen(DestroyFrozenFundsScreen::new( - identity_token_balance.clone(), - app_context, - )) - } - ScreenType::ResumeTokensScreen(identity_token_balance) => { - Screen::DestroyFrozenFundsScreen(DestroyFrozenFundsScreen::new( - identity_token_balance.clone(), + identity_token_info.clone(), app_context, )) } + ScreenType::FreezeTokensScreen(identity_token_info) => Screen::FreezeTokensScreen( + FreezeTokensScreen::new(identity_token_info.clone(), app_context), + ), + ScreenType::UnfreezeTokensScreen(identity_token_info) => Screen::UnfreezeTokensScreen( + UnfreezeTokensScreen::new(identity_token_info.clone(), app_context), + ), + ScreenType::PauseTokensScreen(identity_token_info) => Screen::PauseTokensScreen( + PauseTokensScreen::new(identity_token_info.clone(), app_context), + ), + ScreenType::ResumeTokensScreen(identity_token_info) => Screen::ResumeTokensScreen( + ResumeTokensScreen::new(identity_token_info.clone(), app_context), + ), ScreenType::ClaimTokensScreen(_) => { unreachable!() } @@ -359,9 +347,9 @@ impl ScreenType { app_context, )) } - ScreenType::UpdateTokenConfigScreen(identity_token_balance) => { + ScreenType::UpdateTokenConfigScreen(identity_token_info) => { Screen::UpdateTokenConfigScreen(UpdateTokenConfigScreen::new( - identity_token_balance.clone(), + identity_token_info.clone(), app_context, )) } @@ -574,22 +562,22 @@ impl Screen { ScreenType::MintTokensScreen(screen.identity_token_info.clone()) } Screen::BurnTokensScreen(screen) => { - ScreenType::BurnTokensScreen(screen.identity_token_balance.clone()) + ScreenType::BurnTokensScreen(screen.identity_token_info.clone()) } Screen::DestroyFrozenFundsScreen(screen) => { - ScreenType::DestroyFrozenFundsScreen(screen.identity_token_balance.clone()) + ScreenType::DestroyFrozenFundsScreen(screen.identity_token_info.clone()) } Screen::FreezeTokensScreen(screen) => { - ScreenType::FreezeTokensScreen(screen.identity_token_balance.clone()) + ScreenType::FreezeTokensScreen(screen.identity_token_info.clone()) } Screen::UnfreezeTokensScreen(screen) => { - ScreenType::UnfreezeTokensScreen(screen.identity_token_balance.clone()) + ScreenType::UnfreezeTokensScreen(screen.identity_token_info.clone()) } Screen::PauseTokensScreen(screen) => { - ScreenType::PauseTokensScreen(screen.identity_token_balance.clone()) + ScreenType::PauseTokensScreen(screen.identity_token_info.clone()) } Screen::ResumeTokensScreen(screen) => { - ScreenType::ResumeTokensScreen(screen.identity_token_balance.clone()) + ScreenType::ResumeTokensScreen(screen.identity_token_info.clone()) } Screen::ClaimTokensScreen(screen) => { ScreenType::ClaimTokensScreen(screen.identity_token_balance.clone()) @@ -598,7 +586,7 @@ impl Screen { ScreenType::ViewTokenClaimsScreen(screen.identity_token_balance.clone()) } Screen::UpdateTokenConfigScreen(screen) => { - ScreenType::UpdateTokenConfigScreen(screen.identity_token_balance.clone()) + ScreenType::UpdateTokenConfigScreen(screen.identity_token_info.clone()) } Screen::AddTokenById(_) => ScreenType::AddTokenById, Screen::PurchaseTokenScreen(screen) => { diff --git a/src/ui/tokens/add_token_by_id_screen.rs b/src/ui/tokens/add_token_by_id_screen.rs index b1ced076c..c5296f967 100644 --- a/src/ui/tokens/add_token_by_id_screen.rs +++ b/src/ui/tokens/add_token_by_id_screen.rs @@ -137,9 +137,17 @@ impl AddTokenByIdScreen { InsertTokensToo::SomeTokensShouldBeAdded(vec![tok.token_position]); // None for alias; change if you allow user alias input - return AppAction::BackendTask(BackendTask::ContractTask( - ContractTask::SaveDataContract(contract.clone(), None, insert_mode), - )); + return AppAction::BackendTasks( + vec![ + BackendTask::ContractTask(ContractTask::SaveDataContract( + contract.clone(), + None, + insert_mode, + )), + BackendTask::TokenTask(TokenTask::QueryMyTokenBalances), + ], + crate::app::BackendTasksExecutionMode::Sequential, + ); } } AppAction::None diff --git a/src/ui/tokens/burn_tokens_screen.rs b/src/ui/tokens/burn_tokens_screen.rs index d84635245..493f0cb1e 100644 --- a/src/ui/tokens/burn_tokens_screen.rs +++ b/src/ui/tokens/burn_tokens_screen.rs @@ -3,22 +3,28 @@ use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use dash_sdk::dpp::data_contract::group::Group; +use dash_sdk::dpp::data_contract::GroupContractPosition; +use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use eframe::egui::{self, Color32, Context, Ui}; use egui::RichText; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; +use crate::ui::helpers::render_group_action_text; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; -use dash_sdk::platform::IdentityPublicKey; +use dash_sdk::platform::{Identifier, IdentityPublicKey}; use crate::app::{AppAction, BackendTasksExecutionMode}; use crate::backend_task::tokens::TokenTask; use crate::backend_task::BackendTask; use crate::context::AppContext; -use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; @@ -27,7 +33,7 @@ use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::{MessageType, Screen, ScreenLike}; -use super::tokens_screen::IdentityTokenBalance; +use super::tokens_screen::IdentityTokenInfo; /// Internal states for the burn process. #[derive(PartialEq)] @@ -39,12 +45,13 @@ pub enum BurnTokensStatus { } pub struct BurnTokensScreen { - pub identity: QualifiedIdentity, - pub identity_token_balance: IdentityTokenBalance, // Info on which token/contract to burn from + pub identity_token_info: IdentityTokenInfo, selected_key: Option, + group: Option<(GroupContractPosition, Group)>, + pub group_action_id: Option, // The user chooses how many tokens to burn - amount_to_burn: String, + pub amount_to_burn: String, public_note: Option, status: BurnTokensStatus, @@ -63,40 +70,102 @@ pub struct BurnTokensScreen { } impl BurnTokensScreen { - pub fn new( - identity_token_balance: IdentityTokenBalance, - app_context: &Arc, - ) -> Self { - // Find the local qualified identity that corresponds to `identity_token_balance.identity_id` - let identity = app_context - .load_local_qualified_identities() - .unwrap_or_default() - .into_iter() - .find(|id| id.identity.id() == identity_token_balance.identity_id) - .expect("No local qualified identity found matching this token's identity."); - - // Grab a default signing key if possible - let identity_clone = identity.identity.clone(); - let possible_key = identity_clone.get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); + pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + let possible_key = identity_token_info + .identity + .identity + .get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ) + .cloned(); - // Attempt to get an unlocked wallet reference let mut error_message = None; - let selected_wallet = - get_selected_wallet(&identity, None, possible_key.clone(), &mut error_message); + + let group = match identity_token_info + .token_config + .manual_burning_rules() + .authorized_to_make_change_action_takers() + { + AuthorizedActionTakers::NoOne => { + error_message = Some("Burning is not allowed on this token".to_string()); + None + } + AuthorizedActionTakers::ContractOwner => { + if identity_token_info.data_contract.contract.owner_id() + != &identity_token_info.identity.identity.id() + { + error_message = Some( + "You are not allowed to burn this token. Only the contract owner is." + .to_string(), + ); + } + None + } + AuthorizedActionTakers::Identity(identifier) => { + if identifier != &identity_token_info.identity.identity.id() { + error_message = Some("You are not allowed to burn this token".to_string()); + } + None + } + AuthorizedActionTakers::MainGroup => { + match identity_token_info.token_config.main_control_group() { + None => { + error_message = Some( + "Invalid contract: No main control group, though one should exist" + .to_string(), + ); + None + } + Some(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(group_pos) + { + Ok(group) => Some((group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + } + } + AuthorizedActionTakers::Group(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(*group_pos) + { + Ok(group) => Some((*group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + }; + + // Attempt to get an unlocked wallet reference + let selected_wallet = get_selected_wallet( + &identity_token_info.identity, + None, + possible_key.as_ref(), + &mut error_message, + ); Self { - identity, - identity_token_balance, - selected_key: possible_key.cloned(), + identity_token_info, + selected_key: possible_key, + group, + group_action_id: None, amount_to_burn: String::new(), public_note: None, status: BurnTokensStatus::NotStarted, - error_message: None, + error_message, app_context: app_context.clone(), show_confirmation_popup: false, selected_wallet, @@ -117,7 +186,13 @@ impl BurnTokensScreen { .show_ui(ui, |ui| { if self.app_context.developer_mode { // Show all loaded public keys - for key in self.identity.identity.public_keys().values() { + for key in self + .identity_token_info + .identity + .identity + .public_keys() + .values() + { let is_valid = key.purpose() == Purpose::AUTHENTICATION && key.security_level() == SecurityLevel::CRITICAL; @@ -143,6 +218,7 @@ impl BurnTokensScreen { } else { // Show only "available" auth keys for key_wrapper in self + .identity_token_info .identity .available_authentication_keys_with_critical_security_level() { @@ -208,21 +284,41 @@ impl BurnTokensScreen { .get_contracts(None, None) .expect("Contracts not loaded") .iter() - .find(|c| c.contract.id() == self.identity_token_balance.data_contract_id) + .find(|c| { + c.contract.id() == self.identity_token_info.data_contract.contract.id() + }) .expect("Data contract not found") .contract .clone(); + let group_info; + if self.group_action_id.is_some() { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }); + } else { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }); + } + // Dispatch the actual backend burn action action = AppAction::BackendTasks( vec![ BackendTask::TokenTask(TokenTask::BurnTokens { - owner_identity: self.identity.clone(), + owner_identity: self.identity_token_info.identity.clone(), data_contract, - token_position: self.identity_token_balance.token_position, + token_position: self.identity_token_info.token_position, signing_key: self.selected_key.clone().expect("Expected a key"), public_note: self.public_note.clone(), amount: amount_ok.unwrap(), + group_info, }), BackendTask::TokenTask(TokenTask::QueryMyTokenBalances), ], @@ -249,11 +345,21 @@ impl BurnTokensScreen { ui.add_space(50.0); ui.heading("🎉"); - ui.heading("Burn Successful!"); + if self.group_action_id.is_some() { + ui.label("Group Burn Signing Successful."); + } else { + ui.heading("Burn Successful."); + } ui.add_space(20.0); - if ui.button("Back to Tokens").clicked() { + let button_text; + if self.group_action_id.is_some() { + button_text = "Back to Group Actions"; + } else { + button_text = "Back to Tokens"; + } + if ui.button(button_text).clicked() { // Pop this screen and refresh action = AppAction::PopScreenAndRefresh; } @@ -285,27 +391,40 @@ impl ScreenLike for BurnTokensScreen { if let Ok(all_identities) = self.app_context.load_local_qualified_identities() { if let Some(updated_identity) = all_identities .into_iter() - .find(|id| id.identity.id() == self.identity.identity.id()) + .find(|id| id.identity.id() == self.identity_token_info.identity.identity.id()) { - self.identity = updated_identity; + self.identity_token_info.identity = updated_identity; } } } fn ui(&mut self, ctx: &Context) -> AppAction { - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Tokens", AppAction::GoToMainScreen), - ( - &self.identity_token_balance.token_alias, - AppAction::PopScreen, - ), - ("Burn", AppAction::None), - ], - vec![], - ); + let mut action; + + // Build a top panel + if self.group_action_id.is_some() { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Group Actions", AppAction::PopScreen), + ("Burn", AppAction::None), + ], + vec![], + ); + } else { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Tokens", AppAction::GoToMainScreen), + (&self.identity_token_info.token_alias, AppAction::PopScreen), + ("Burn", AppAction::None), + ], + vec![], + ); + } // Left panel action |= add_left_panel( @@ -329,11 +448,17 @@ impl ScreenLike for BurnTokensScreen { // Check if user has any auth keys let has_keys = if self.app_context.developer_mode { - !self.identity.identity.public_keys().is_empty() + !self + .identity_token_info + .identity + .identity + .public_keys() + .is_empty() } else { !self + .identity_token_info .identity - .available_authentication_keys_with_critical_security_level() + .available_authentication_keys() .is_empty() }; @@ -342,23 +467,29 @@ impl ScreenLike for BurnTokensScreen { Color32::DARK_RED, format!( "No authentication keys found for this {} identity.", - self.identity.identity_type, + self.identity_token_info.identity.identity_type, ), ); ui.add_space(10.0); // Show "Add key" or "Check keys" option - let first_key = self.identity.identity.get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); + let first_key = self + .identity_token_info + .identity + .identity + .get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([ + SecurityLevel::CRITICAL, + ]), + KeyType::all_key_types().into(), + false, + ); if let Some(key) = first_key { if ui.button("Check Keys").clicked() { action |= AppAction::AddScreen(Screen::KeyInfoScreen(KeyInfoScreen::new( - self.identity.clone(), + self.identity_token_info.identity.clone(), key.clone(), None, &self.app_context, @@ -369,7 +500,7 @@ impl ScreenLike for BurnTokensScreen { if ui.button("Add key").clicked() { action |= AppAction::AddScreen(Screen::AddKeyScreen(AddKeyScreen::new( - self.identity.clone(), + self.identity_token_info.identity.clone(), &self.app_context, ))); } @@ -390,9 +521,14 @@ impl ScreenLike for BurnTokensScreen { ui.horizontal(|ui| { self.render_key_selection(ui); ui.add_space(5.0); - let identity_id_string = - self.identity.identity.id().to_string(Encoding::Base58); + let identity_id_string = self + .identity_token_info + .identity + .identity + .id() + .to_string(Encoding::Base58); let identity_display = self + .identity_token_info .identity .alias .as_deref() @@ -407,7 +543,18 @@ impl ScreenLike for BurnTokensScreen { // 2) Amount to burn ui.heading("2. Amount to burn"); ui.add_space(5.0); - self.render_amount_input(ui); + if self.group_action_id.is_some() { + ui.label( + "You are signing an existing group Burn so you are not allowed to choose the amount.", + ); + ui.add_space(5.0); + ui.label(format!( + "Amount: {}", + self.amount_to_burn + )); + } else { + self.render_amount_input(ui); + } ui.add_space(10.0); ui.separator(); @@ -427,18 +574,28 @@ impl ScreenLike for BurnTokensScreen { ) .changed() { - self.public_note = Some(txt); + self.public_note = if txt.len() > 0 { + Some(txt) + } else { + None + }; } }); - ui.add_space(10.0); + + let button_text = + render_group_action_text(ui, &self.group, &self.identity_token_info, "Burn", &self.group_action_id); // Burn button - let button = egui::Button::new(RichText::new("Burn").color(Color32::WHITE)) - .fill(Color32::from_rgb(255, 0, 0)) - .corner_radius(3.0); + if self.app_context.developer_mode || !button_text.contains("Test") { + ui.add_space(10.0); + let button = + egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() { + self.show_confirmation_popup = true; + } } // If user pressed "Burn," show a popup diff --git a/src/ui/tokens/claim_tokens_screen.rs b/src/ui/tokens/claim_tokens_screen.rs index 3fb771428..424fcd42e 100644 --- a/src/ui/tokens/claim_tokens_screen.rs +++ b/src/ui/tokens/claim_tokens_screen.rs @@ -111,7 +111,7 @@ impl ClaimTokensScreen { token_configuration, distribution_type, status: ClaimTokensStatus::NotStarted, - error_message: None, + error_message, app_context: app_context.clone(), show_confirmation_popup: false, selected_wallet, diff --git a/src/ui/tokens/destroy_frozen_funds_screen.rs b/src/ui/tokens/destroy_frozen_funds_screen.rs index 1f371203c..afab8e2eb 100644 --- a/src/ui/tokens/destroy_frozen_funds_screen.rs +++ b/src/ui/tokens/destroy_frozen_funds_screen.rs @@ -3,12 +3,19 @@ use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use dash_sdk::dpp::data_contract::group::Group; +use dash_sdk::dpp::data_contract::GroupContractPosition; +use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use eframe::egui::{self, Color32, Context, Ui}; use egui::RichText; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; +use crate::ui::helpers::render_group_action_text; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; @@ -27,7 +34,7 @@ use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::{MessageType, Screen, ScreenLike}; -use super::tokens_screen::IdentityTokenBalance; +use super::tokens_screen::IdentityTokenInfo; /// Represents possible states in the “destroy frozen funds” flow #[derive(PartialEq)] @@ -44,17 +51,20 @@ pub struct DestroyFrozenFundsScreen { pub identity: QualifiedIdentity, /// Info on which token contract we’re dealing with - pub identity_token_balance: IdentityTokenBalance, + pub identity_token_info: IdentityTokenInfo, /// The key used to sign the operation selected_key: Option, + group: Option<(GroupContractPosition, Group)>, + pub group_action_id: Option, + /// Optional public note pub public_note: Option, /// The user must specify the identity ID whose frozen funds are to be destroyed /// Typically some Identity that has been frozen by the system or a group - frozen_identity_id: String, + pub frozen_identity_id: String, status: DestroyFrozenFundsStatus, error_message: Option, @@ -72,40 +82,103 @@ pub struct DestroyFrozenFundsScreen { } impl DestroyFrozenFundsScreen { - pub fn new( - identity_token_balance: IdentityTokenBalance, - app_context: &Arc, - ) -> Self { - // Locate the local identity that owns the token contract - let identity = app_context - .load_local_qualified_identities() - .unwrap_or_default() - .into_iter() - .find(|id| id.identity.id() == identity_token_balance.identity_id) - .expect("No local qualified identity found matching this token's identity."); - - // Grab a suitable key - let identity_clone = identity.identity.clone(); - let possible_key = identity_clone.get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); + pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + let possible_key = identity_token_info + .identity + .identity + .get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ) + .cloned(); - // Possibly get an unlocked wallet let mut error_message = None; - let selected_wallet = - get_selected_wallet(&identity, None, possible_key.clone(), &mut error_message); + + let group = match identity_token_info + .token_config + .destroy_frozen_funds_rules() + .authorized_to_make_change_action_takers() + { + AuthorizedActionTakers::NoOne => { + error_message = Some("Burning is not allowed on this token".to_string()); + None + } + AuthorizedActionTakers::ContractOwner => { + if identity_token_info.data_contract.contract.owner_id() + != &identity_token_info.identity.identity.id() + { + error_message = Some( + "You are not allowed to burn this token. Only the contract owner is." + .to_string(), + ); + } + None + } + AuthorizedActionTakers::Identity(identifier) => { + if identifier != &identity_token_info.identity.identity.id() { + error_message = Some("You are not allowed to burn this token".to_string()); + } + None + } + AuthorizedActionTakers::MainGroup => { + match identity_token_info.token_config.main_control_group() { + None => { + error_message = Some( + "Invalid contract: No main control group, though one should exist" + .to_string(), + ); + None + } + Some(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(group_pos) + { + Ok(group) => Some((group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + } + } + AuthorizedActionTakers::Group(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(*group_pos) + { + Ok(group) => Some((*group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + }; + + // Attempt to get an unlocked wallet reference + let selected_wallet = get_selected_wallet( + &identity_token_info.identity, + None, + possible_key.as_ref(), + &mut error_message, + ); Self { - identity, - identity_token_balance, - selected_key: possible_key.cloned(), - public_note: None, + identity: identity_token_info.identity.clone(), frozen_identity_id: String::new(), + identity_token_info, + selected_key: possible_key, + group, + group_action_id: None, + public_note: None, status: DestroyFrozenFundsStatus::NotStarted, - error_message: None, + error_message, app_context: app_context.clone(), show_confirmation_popup: false, selected_wallet, @@ -226,21 +299,41 @@ impl DestroyFrozenFundsScreen { .get_contracts(None, None) .expect("Contracts not loaded") .iter() - .find(|c| c.contract.id() == self.identity_token_balance.data_contract_id) + .find(|c| { + c.contract.id() == self.identity_token_info.data_contract.contract.id() + }) .expect("Data contract not found") .contract .clone(); + let group_info; + if self.group_action_id.is_some() { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }); + } else { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }); + } + // Dispatch the actual backend destroy action action = AppAction::BackendTasks( vec![ BackendTask::TokenTask(TokenTask::DestroyFrozenFunds { actor_identity: self.identity.clone(), data_contract, - token_position: self.identity_token_balance.token_position, + token_position: self.identity_token_info.token_position, signing_key: self.selected_key.clone().expect("Expected a key"), public_note: self.public_note.clone(), frozen_identity: frozen_id, + group_info, }), BackendTask::TokenTask(TokenTask::QueryMyTokenBalances), ], @@ -267,11 +360,21 @@ impl DestroyFrozenFundsScreen { ui.add_space(50.0); ui.heading("🎉"); - ui.heading("Successfully destroyed frozen funds!"); + if self.group_action_id.is_some() { + ui.label("Group Destroy Signing Successful."); + } else { + ui.heading("Destroy Successful."); + } ui.add_space(20.0); - if ui.button("Back to Tokens").clicked() { + let button_text; + if self.group_action_id.is_some() { + button_text = "Back to Group Actions"; + } else { + button_text = "Back to Tokens"; + } + if ui.button(button_text).clicked() { action = AppAction::PopScreenAndRefresh; } }); @@ -314,19 +417,32 @@ impl ScreenLike for DestroyFrozenFundsScreen { } fn ui(&mut self, ctx: &Context) -> AppAction { - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Tokens", AppAction::GoToMainScreen), - ( - &self.identity_token_balance.token_alias, - AppAction::PopScreen, - ), - ("Destroy", AppAction::None), - ], - vec![], - ); + let mut action; + + // Build a top panel + if self.group_action_id.is_some() { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Group Actions", AppAction::PopScreen), + ("Destroy Frozen Funds", AppAction::None), + ], + vec![], + ); + } else { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Tokens", AppAction::GoToMainScreen), + (&self.identity_token_info.token_alias, AppAction::PopScreen), + ("Destroy Frozen Funds", AppAction::None), + ], + vec![], + ); + } // Left panel action |= add_left_panel( @@ -425,7 +541,18 @@ impl ScreenLike for DestroyFrozenFundsScreen { // Frozen identity ui.heading("2. Frozen identity to destroy funds from"); ui.add_space(5.0); - self.render_frozen_identity_input(ui); + if self.group_action_id.is_some() { + ui.label( + "You are signing an existing group Destroy so you are not allowed to choose the identity.", + ); + ui.add_space(5.0); + ui.label(format!( + "Identity: {}", + self.frozen_identity_id + )); + } else { + self.render_frozen_identity_input(ui); + } ui.add_space(10.0); ui.separator(); @@ -445,18 +572,33 @@ impl ScreenLike for DestroyFrozenFundsScreen { ) .changed() { - self.public_note = Some(txt); + self.public_note = if txt.len() > 0 { + Some(txt) + } else { + None + }; } }); - ui.add_space(10.0); + + let button_text = render_group_action_text( + ui, + &self.group, + &self.identity_token_info, + "Destroy Frozen Funds", + &self.group_action_id, + ); // Destroy button - let button = egui::Button::new(RichText::new("Destroy").color(Color32::WHITE)) - .fill(Color32::DARK_RED) - .corner_radius(3.0); + if self.app_context.developer_mode || !button_text.contains("Test") { + ui.add_space(10.0); + let button = + egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() { + self.show_confirmation_popup = true; + } } // If user pressed "Destroy," show a popup diff --git a/src/ui/tokens/freeze_tokens_screen.rs b/src/ui/tokens/freeze_tokens_screen.rs index 5b6d45f66..f540dba7f 100644 --- a/src/ui/tokens/freeze_tokens_screen.rs +++ b/src/ui/tokens/freeze_tokens_screen.rs @@ -3,6 +3,12 @@ use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use dash_sdk::dpp::data_contract::group::Group; +use dash_sdk::dpp::data_contract::GroupContractPosition; +use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use eframe::egui::{self, Color32, Context, Ui}; use egui::RichText; @@ -22,12 +28,13 @@ use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::helpers::render_group_action_text; use crate::ui::identities::get_selected_wallet; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::{MessageType, Screen, ScreenLike}; -use super::tokens_screen::IdentityTokenBalance; +use super::tokens_screen::IdentityTokenInfo; /// Internal states for the freeze operation #[derive(PartialEq)] @@ -41,12 +48,15 @@ pub enum FreezeTokensStatus { /// A UI Screen that allows freezing an identity’s tokens for a particular contract pub struct FreezeTokensScreen { pub identity: QualifiedIdentity, - pub identity_token_balance: IdentityTokenBalance, + pub identity_token_info: IdentityTokenInfo, selected_key: Option, public_note: Option, + group: Option<(GroupContractPosition, Group)>, + pub group_action_id: Option, + /// The identity we want to freeze - freeze_identity_id: String, + pub freeze_identity_id: String, status: FreezeTokensStatus, error_message: Option, @@ -64,37 +74,103 @@ pub struct FreezeTokensScreen { } impl FreezeTokensScreen { - pub fn new( - identity_token_balance: IdentityTokenBalance, - app_context: &Arc, - ) -> Self { - let identity = app_context - .load_local_qualified_identities() - .unwrap_or_default() - .into_iter() - .find(|id| id.identity.id() == identity_token_balance.identity_id) - .expect("No local qualified identity found for this token"); - - let identity_clone = identity.identity.clone(); - let possible_key = identity_clone.get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); + pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + let possible_key = identity_token_info + .identity + .identity + .get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ) + .cloned(); let mut error_message = None; - let selected_wallet = - get_selected_wallet(&identity, None, possible_key.clone(), &mut error_message); + + let group = match identity_token_info + .token_config + .freeze_rules() + .authorized_to_make_change_action_takers() + { + AuthorizedActionTakers::NoOne => { + error_message = Some("Burning is not allowed on this token".to_string()); + None + } + AuthorizedActionTakers::ContractOwner => { + if identity_token_info.data_contract.contract.owner_id() + != &identity_token_info.identity.identity.id() + { + error_message = Some( + "You are not allowed to burn this token. Only the contract owner is." + .to_string(), + ); + } + None + } + AuthorizedActionTakers::Identity(identifier) => { + if identifier != &identity_token_info.identity.identity.id() { + error_message = Some("You are not allowed to burn this token".to_string()); + } + None + } + AuthorizedActionTakers::MainGroup => { + match identity_token_info.token_config.main_control_group() { + None => { + error_message = Some( + "Invalid contract: No main control group, though one should exist" + .to_string(), + ); + None + } + Some(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(group_pos) + { + Ok(group) => Some((group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + } + } + AuthorizedActionTakers::Group(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(*group_pos) + { + Ok(group) => Some((*group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + }; + + // Attempt to get an unlocked wallet reference + let selected_wallet = get_selected_wallet( + &identity_token_info.identity, + None, + possible_key.as_ref(), + &mut error_message, + ); Self { - identity, - identity_token_balance, - selected_key: possible_key.cloned(), + identity: identity_token_info.identity.clone(), + identity_token_info, + selected_key: possible_key, + group, + group_action_id: None, public_note: None, freeze_identity_id: String::new(), status: FreezeTokensStatus::NotStarted, - error_message: None, + error_message, app_context: app_context.clone(), show_confirmation_popup: false, selected_wallet, @@ -212,20 +288,40 @@ impl FreezeTokensScreen { .get_contracts(None, None) .expect("Contracts not loaded") .iter() - .find(|c| c.contract.id() == self.identity_token_balance.data_contract_id) + .find(|c| { + c.contract.id() == self.identity_token_info.data_contract.contract.id() + }) .expect("Data contract not found") .contract .clone(); + let group_info; + if self.group_action_id.is_some() { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }); + } else { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }); + } + // Dispatch to backend action = AppAction::BackendTask(BackendTask::TokenTask(TokenTask::FreezeTokens { actor_identity: self.identity.clone(), data_contract, - token_position: self.identity_token_balance.token_position, + token_position: self.identity_token_info.token_position, signing_key: self.selected_key.clone().expect("No key selected"), public_note: self.public_note.clone(), freeze_identity: freeze_id, + group_info, })); } @@ -248,11 +344,21 @@ impl FreezeTokensScreen { ui.add_space(50.0); ui.heading("🎉"); - ui.heading("Frozen Successfully!"); + if self.group_action_id.is_some() { + ui.label("Group Freeze Signing Successful."); + } else { + ui.heading("Freeze Successful."); + } ui.add_space(20.0); - if ui.button("Back to Tokens").clicked() { + let button_text; + if self.group_action_id.is_some() { + button_text = "Back to Group Actions"; + } else { + button_text = "Back to Tokens"; + } + if ui.button(button_text).clicked() { action = AppAction::PopScreenAndRefresh; } }); @@ -290,19 +396,32 @@ impl ScreenLike for FreezeTokensScreen { } fn ui(&mut self, ctx: &Context) -> AppAction { - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Tokens", AppAction::GoToMainScreen), - ( - &self.identity_token_balance.token_alias, - AppAction::PopScreen, - ), - ("Freeze", AppAction::None), - ], - vec![], - ); + let mut action; + + // Build a top panel + if self.group_action_id.is_some() { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Group Actions", AppAction::PopScreen), + ("Freeze", AppAction::None), + ], + vec![], + ); + } else { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Tokens", AppAction::GoToMainScreen), + (&self.identity_token_info.token_alias, AppAction::PopScreen), + ("Freeze", AppAction::None), + ], + vec![], + ); + } // Left panel action |= add_left_panel( @@ -402,7 +521,18 @@ impl ScreenLike for FreezeTokensScreen { // 2) Identity to freeze ui.heading("2. Enter the identity ID to freeze"); ui.add_space(5.0); - self.render_freeze_identity_input(ui); + if self.group_action_id.is_some() { + ui.label( + "You are signing an existing group Freeze so you are not allowed to choose the identity.", + ); + ui.add_space(5.0); + ui.label(format!( + "Identity: {}", + self.freeze_identity_id + )); + } else { + self.render_freeze_identity_input(ui); + } ui.add_space(10.0); ui.separator(); @@ -422,18 +552,28 @@ impl ScreenLike for FreezeTokensScreen { ) .changed() { - self.public_note = Some(txt); + self.public_note = if txt.len() > 0 { + Some(txt) + } else { + None + }; } }); - ui.add_space(10.0); + + let button_text = + render_group_action_text(ui, &self.group, &self.identity_token_info, "Freeze", &self.group_action_id); // Freeze button - let button = egui::Button::new(RichText::new("Freeze").color(Color32::WHITE)) - .fill(Color32::from_rgb(64, 64, 255)) - .corner_radius(3.0); + if self.app_context.developer_mode || !button_text.contains("Test") { + ui.add_space(10.0); + let button = + egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() { + self.show_confirmation_popup = true; + } } // If user pressed "Freeze," show popup diff --git a/src/ui/tokens/mint_tokens_screen.rs b/src/ui/tokens/mint_tokens_screen.rs index c225b3f15..cd420ccad 100644 --- a/src/ui/tokens/mint_tokens_screen.rs +++ b/src/ui/tokens/mint_tokens_screen.rs @@ -7,7 +7,6 @@ use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; use dash_sdk::dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; -use dash_sdk::dpp::data_contract::group::accessors::v0::GroupV0Getters; use dash_sdk::dpp::data_contract::group::Group; use dash_sdk::dpp::data_contract::GroupContractPosition; use eframe::egui::{self, Color32, Context, Ui}; @@ -23,11 +22,12 @@ use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::helpers::render_group_action_text; use crate::ui::identities::get_selected_wallet; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::{MessageType, Screen, ScreenLike}; -use dash_sdk::dpp::group::GroupStateTransitionInfoStatus; +use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; @@ -49,10 +49,11 @@ pub struct MintTokensScreen { selected_key: Option, public_note: Option, group: Option<(GroupContractPosition, Group)>, + pub group_action_id: Option, - recipient_identity_id: String, + pub recipient_identity_id: String, - amount_to_mint: String, + pub amount_to_mint: String, status: MintTokensStatus, error_message: Option, @@ -97,7 +98,7 @@ impl MintTokensScreen { != &identity_token_info.identity.identity.id() { error_message = Some( - "You are not allowed to mint on this token. Only the contract owner is." + "You are not allowed to mint this token. Only the contract owner is." .to_string(), ); } @@ -105,7 +106,7 @@ impl MintTokensScreen { } AuthorizedActionTakers::Identity(identifier) => { if identifier != &identity_token_info.identity.identity.id() { - error_message = Some("You are not allowed to mint on this token".to_string()); + error_message = Some("You are not allowed to mint this token".to_string()); } None } @@ -161,10 +162,11 @@ impl MintTokensScreen { selected_key: possible_key, public_note: None, group, + group_action_id: None, recipient_identity_id: "".to_string(), amount_to_mint: "".to_string(), status: MintTokensStatus::NotStarted, - error_message: None, + error_message, app_context: app_context.clone(), show_confirmation_popup: false, selected_wallet, @@ -319,9 +321,22 @@ impl MintTokensScreen { .as_secs(); self.status = MintTokensStatus::WaitingForResult(now); - let group_info = self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }); + let group_info; + if self.group_action_id.is_some() { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }); + } else { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }); + } // Dispatch the actual backend mint action action = AppAction::BackendTasks( @@ -365,11 +380,21 @@ impl MintTokensScreen { ui.add_space(50.0); ui.heading("🎉"); - ui.heading("Mint Successful!"); + if self.group_action_id.is_some() { + ui.label("Group Mint Signing Successful."); + } else { + ui.heading("Mint Successful."); + } ui.add_space(20.0); - if ui.button("Back to Tokens").clicked() { + let button_text; + if self.group_action_id.is_some() { + button_text = "Back to Group Actions"; + } else { + button_text = "Back to Tokens"; + } + if ui.button(button_text).clicked() { // Pop this screen and refresh action = AppAction::PopScreenAndRefresh; } @@ -409,17 +434,32 @@ impl ScreenLike for MintTokensScreen { } fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action; + // Build a top panel - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Tokens", AppAction::GoToMainScreen), - (&self.identity_token_info.token_alias, AppAction::PopScreen), - ("Mint", AppAction::None), - ], - vec![], - ); + if self.group_action_id.is_some() { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Group Actions", AppAction::PopScreen), + ("Mint", AppAction::None), + ], + vec![], + ); + } else { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Tokens", AppAction::GoToMainScreen), + (&self.identity_token_info.token_alias, AppAction::PopScreen), + ("Mint", AppAction::None), + ], + vec![], + ); + } // Left panel action |= add_left_panel( @@ -443,7 +483,12 @@ impl ScreenLike for MintTokensScreen { // Check if user has any auth keys let has_keys = if self.app_context.developer_mode { - !self.identity_token_info.identity.identity.public_keys().is_empty() + !self + .identity_token_info + .identity + .identity + .public_keys() + .is_empty() } else { !self.identity_token_info.identity.available_authentication_keys_with_critical_security_level().is_empty() }; @@ -503,9 +548,15 @@ impl ScreenLike for MintTokensScreen { ui.horizontal(|ui| { self.render_key_selection(ui); ui.add_space(5.0); - let identity_id_string = - self.identity_token_info.identity.identity.id().to_string(Encoding::Base58); - let identity_display = self.identity_token_info.identity + let identity_id_string = self + .identity_token_info + .identity + .identity + .id() + .to_string(Encoding::Base58); + let identity_display = self + .identity_token_info + .identity .alias .as_deref() .unwrap_or_else(|| &identity_id_string); @@ -519,14 +570,37 @@ impl ScreenLike for MintTokensScreen { // 2) Amount to mint ui.heading("2. Amount to mint"); ui.add_space(5.0); - self.render_amount_input(ui); + if self.group_action_id.is_some() { + ui.label( + "You are signing an existing group Mint so you are not allowed to choose the amount.", + ); + ui.add_space(5.0); + ui.label(format!( + "Amount: {}", + self.amount_to_mint + )); + } else { + self.render_amount_input(ui); + } - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); + if self + .identity_token_info + .token_config + .distribution_rules() + .minting_allow_choosing_destination() + || self.app_context.developer_mode + { + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); - if self.identity_token_info.token_config.distribution_rules().minting_allow_choosing_destination() || self.app_context.developer_mode { - if self.identity_token_info.token_config.distribution_rules().new_tokens_destination_identity().is_some() { + if self + .identity_token_info + .token_config + .distribution_rules() + .new_tokens_destination_identity() + .is_some() + { ui.heading("3. Recipient identity (optional)"); } else { ui.heading("3. Recipient identity (required)"); @@ -553,47 +627,28 @@ impl ScreenLike for MintTokensScreen { ) .changed() { - self.public_note = Some(txt); + self.public_note = if txt.len() > 0 { + Some(txt) + } else { + None + }; } }); - ui.add_space(10.0); - let mint_text = if let Some((_, group)) = self.group.as_ref() { - let your_power = group.members().get(&self.identity_token_info.identity.identity.id()); - if your_power.is_none() { - self.error_message = Some("Only group members can mint on this token".to_string()); - } - ui.heading("This is a group action, it is not immediate."); - ui.label(format!("Members are : \n{}", group.members().iter().map(|(member, power)| { - if member == &self.identity_token_info.identity.identity.id() { - format!("{} (You) with power {}", member, power) - } else { - format!("{} with power {}", member, power) - } - }).collect::>().join(", \n"))); - ui.add_space(10.0); - if let Some(your_power) = your_power { - if *your_power >= group.required_power() { - ui.label(format!("Even though this is a group action, you are able to unilaterally approve it because your power ({}) in the group exceeds the required amount : {}", *your_power, group.required_power())); - "Mint" - } else { - ui.label(format!("You will need at least {} voting power for this action to go through. Contact other group members to let them know to authorize this action after you have initiated it.", group.required_power())); - "Initiate Group Mint" - } - } else { - "Test Mint (It should fail)" - } - } else { - "Mint" - }; + let button_text = + render_group_action_text(ui, &self.group, &self.identity_token_info, "Mint", &self.group_action_id); // Mint button - let button = egui::Button::new(RichText::new(mint_text).color(Color32::WHITE)) - .fill(Color32::from_rgb(0, 128, 255)) - .corner_radius(3.0); + if self.app_context.developer_mode || !button_text.contains("Test") { + ui.add_space(10.0); + let button = + egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() { + self.show_confirmation_popup = true; + } } // If the user pressed "Mint," show a popup diff --git a/src/ui/tokens/pause_tokens_screen.rs b/src/ui/tokens/pause_tokens_screen.rs index 6a782300f..e54332790 100644 --- a/src/ui/tokens/pause_tokens_screen.rs +++ b/src/ui/tokens/pause_tokens_screen.rs @@ -3,7 +3,14 @@ use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use dash_sdk::dpp::data_contract::group::Group; +use dash_sdk::dpp::data_contract::GroupContractPosition; +use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::platform::Identifier; use eframe::egui::{self, Color32, Context, Ui}; use egui::RichText; @@ -21,12 +28,13 @@ use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::helpers::render_group_action_text; use crate::ui::identities::get_selected_wallet; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::{MessageType, Screen, ScreenLike}; -use super::tokens_screen::IdentityTokenBalance; +use super::tokens_screen::IdentityTokenInfo; /// Represents states for the pause flow #[derive(PartialEq)] @@ -40,8 +48,10 @@ pub enum PauseTokensStatus { /// A UI screen that allows pausing all token-related actions for a contract pub struct PauseTokensScreen { pub identity: QualifiedIdentity, - pub identity_token_balance: IdentityTokenBalance, + pub identity_token_info: IdentityTokenInfo, selected_key: Option, + group: Option<(GroupContractPosition, Group)>, + pub group_action_id: Option, pub public_note: Option, status: PauseTokensStatus, @@ -60,39 +70,102 @@ pub struct PauseTokensScreen { } impl PauseTokensScreen { - pub fn new( - identity_token_balance: IdentityTokenBalance, - app_context: &Arc, - ) -> Self { - // Find the local identity that owns this token contract - let identity = app_context - .load_local_qualified_identities() - .unwrap_or_default() - .into_iter() - .find(|id| id.identity.id() == identity_token_balance.identity_id) - .expect("No local qualified identity found for this token’s identity."); - - // Grab a suitable key - let identity_clone = identity.identity.clone(); - let possible_key = identity_clone.get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); + pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + let possible_key = identity_token_info + .identity + .identity + .get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ) + .cloned(); - // Possibly get an unlocked wallet let mut error_message = None; - let selected_wallet = - get_selected_wallet(&identity, None, possible_key.clone(), &mut error_message); + + let group = match identity_token_info + .token_config + .emergency_action_rules() + .authorized_to_make_change_action_takers() + { + AuthorizedActionTakers::NoOne => { + error_message = Some("Burning is not allowed on this token".to_string()); + None + } + AuthorizedActionTakers::ContractOwner => { + if identity_token_info.data_contract.contract.owner_id() + != &identity_token_info.identity.identity.id() + { + error_message = Some( + "You are not allowed to burn this token. Only the contract owner is." + .to_string(), + ); + } + None + } + AuthorizedActionTakers::Identity(identifier) => { + if identifier != &identity_token_info.identity.identity.id() { + error_message = Some("You are not allowed to burn this token".to_string()); + } + None + } + AuthorizedActionTakers::MainGroup => { + match identity_token_info.token_config.main_control_group() { + None => { + error_message = Some( + "Invalid contract: No main control group, though one should exist" + .to_string(), + ); + None + } + Some(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(group_pos) + { + Ok(group) => Some((group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + } + } + AuthorizedActionTakers::Group(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(*group_pos) + { + Ok(group) => Some((*group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + }; + + // Attempt to get an unlocked wallet reference + let selected_wallet = get_selected_wallet( + &identity_token_info.identity, + None, + possible_key.as_ref(), + &mut error_message, + ); Self { - identity, - identity_token_balance, - selected_key: possible_key.cloned(), + identity: identity_token_info.identity.clone(), + identity_token_info, + selected_key: possible_key, + group, + group_action_id: None, public_note: None, status: PauseTokensStatus::NotStarted, - error_message: None, + error_message, app_context: app_context.clone(), show_confirmation_popup: false, selected_wallet, @@ -180,18 +253,38 @@ impl PauseTokensScreen { .get_contracts(None, None) .expect("Contracts not loaded") .iter() - .find(|c| c.contract.id() == self.identity_token_balance.data_contract_id) + .find(|c| { + c.contract.id() == self.identity_token_info.data_contract.contract.id() + }) .expect("Data contract not found") .contract .clone(); + let group_info; + if self.group_action_id.is_some() { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }); + } else { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }); + } + action = AppAction::BackendTask(BackendTask::TokenTask(TokenTask::PauseTokens { actor_identity: self.identity.clone(), data_contract, - token_position: self.identity_token_balance.token_position, + token_position: self.identity_token_info.token_position, signing_key: self.selected_key.clone().expect("No key selected"), public_note: self.public_note.clone(), + group_info, })); } @@ -212,11 +305,21 @@ impl PauseTokensScreen { ui.add_space(50.0); ui.heading("🎉"); - ui.heading("Paused Successfully!"); + if self.group_action_id.is_some() { + ui.label("Group Pause Signing Successful."); + } else { + ui.heading("Pause Successful."); + } ui.add_space(20.0); - if ui.button("Back to Tokens").clicked() { + let button_text; + if self.group_action_id.is_some() { + button_text = "Back to Group Actions"; + } else { + button_text = "Back to Tokens"; + } + if ui.button(button_text).clicked() { action = AppAction::PopScreenAndRefresh; } }); @@ -254,19 +357,32 @@ impl ScreenLike for PauseTokensScreen { } fn ui(&mut self, ctx: &Context) -> AppAction { - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Tokens", AppAction::GoToMainScreen), - ( - &self.identity_token_balance.token_alias, - AppAction::PopScreen, - ), - ("Pause", AppAction::None), - ], - vec![], - ); + let mut action; + + // Build a top panel + if self.group_action_id.is_some() { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Group Actions", AppAction::PopScreen), + ("Pause", AppAction::None), + ], + vec![], + ); + } else { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Tokens", AppAction::GoToMainScreen), + (&self.identity_token_info.token_alias, AppAction::PopScreen), + ("Pause", AppAction::None), + ], + vec![], + ); + } // Left panel action |= add_left_panel( @@ -375,17 +491,29 @@ impl ScreenLike for PauseTokensScreen { ) .changed() { - self.public_note = Some(txt); + self.public_note = if txt.len() > 0 { Some(txt) } else { None }; } }); - ui.add_space(10.0); - let button = egui::Button::new(RichText::new("Pause").color(Color32::WHITE)) - .fill(Color32::from_rgb(192, 0, 0)) - .corner_radius(3.0); + let button_text = render_group_action_text( + ui, + &self.group, + &self.identity_token_info, + "Pause", + &self.group_action_id, + ); + + // Pause button + if self.app_context.developer_mode || !button_text.contains("Test") { + ui.add_space(10.0); + let button = + egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() { + self.show_confirmation_popup = true; + } } // If user pressed "Pause," show popup diff --git a/src/ui/tokens/resume_tokens_screen.rs b/src/ui/tokens/resume_tokens_screen.rs index cb684cc5c..fe1d568fd 100644 --- a/src/ui/tokens/resume_tokens_screen.rs +++ b/src/ui/tokens/resume_tokens_screen.rs @@ -3,7 +3,14 @@ use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use dash_sdk::dpp::data_contract::group::Group; +use dash_sdk::dpp::data_contract::GroupContractPosition; +use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::platform::Identifier; use eframe::egui::{self, Color32, Context, Ui}; use egui::RichText; @@ -21,12 +28,13 @@ use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::helpers::render_group_action_text; use crate::ui::identities::get_selected_wallet; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::{MessageType, Screen, ScreenLike}; -use super::tokens_screen::IdentityTokenBalance; +use super::tokens_screen::IdentityTokenInfo; /// States for the resume flow #[derive(PartialEq)] @@ -39,9 +47,11 @@ pub enum ResumeTokensStatus { pub struct ResumeTokensScreen { pub identity: QualifiedIdentity, - pub identity_token_balance: IdentityTokenBalance, + pub identity_token_info: IdentityTokenInfo, selected_key: Option, - public_note: Option, + group: Option<(GroupContractPosition, Group)>, + pub group_action_id: Option, + pub public_note: Option, status: ResumeTokensStatus, error_message: Option, @@ -59,36 +69,102 @@ pub struct ResumeTokensScreen { } impl ResumeTokensScreen { - pub fn new( - identity_token_balance: IdentityTokenBalance, - app_context: &Arc, - ) -> Self { - let identity = app_context - .load_local_qualified_identities() - .unwrap_or_default() - .into_iter() - .find(|id| id.identity.id() == identity_token_balance.identity_id) - .expect("No local qualified identity found for this token’s identity."); - - let identity_clone = identity.identity.clone(); - let possible_key = identity_clone.get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); + pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + let possible_key = identity_token_info + .identity + .identity + .get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ) + .cloned(); let mut error_message = None; - let selected_wallet = - get_selected_wallet(&identity, None, possible_key.clone(), &mut error_message); + + let group = match identity_token_info + .token_config + .emergency_action_rules() + .authorized_to_make_change_action_takers() + { + AuthorizedActionTakers::NoOne => { + error_message = Some("Burning is not allowed on this token".to_string()); + None + } + AuthorizedActionTakers::ContractOwner => { + if identity_token_info.data_contract.contract.owner_id() + != &identity_token_info.identity.identity.id() + { + error_message = Some( + "You are not allowed to burn this token. Only the contract owner is." + .to_string(), + ); + } + None + } + AuthorizedActionTakers::Identity(identifier) => { + if identifier != &identity_token_info.identity.identity.id() { + error_message = Some("You are not allowed to burn this token".to_string()); + } + None + } + AuthorizedActionTakers::MainGroup => { + match identity_token_info.token_config.main_control_group() { + None => { + error_message = Some( + "Invalid contract: No main control group, though one should exist" + .to_string(), + ); + None + } + Some(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(group_pos) + { + Ok(group) => Some((group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + } + } + AuthorizedActionTakers::Group(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(*group_pos) + { + Ok(group) => Some((*group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + }; + + // Attempt to get an unlocked wallet reference + let selected_wallet = get_selected_wallet( + &identity_token_info.identity, + None, + possible_key.as_ref(), + &mut error_message, + ); Self { - identity, - identity_token_balance, - selected_key: possible_key.cloned(), + identity: identity_token_info.identity.clone(), + identity_token_info, + selected_key: possible_key, + group, + group_action_id: None, public_note: None, status: ResumeTokensStatus::NotStarted, - error_message: None, + error_message, app_context: app_context.clone(), show_confirmation_popup: false, selected_wallet, @@ -176,18 +252,38 @@ impl ResumeTokensScreen { .get_contracts(None, None) .expect("Contracts not loaded") .iter() - .find(|c| c.contract.id() == self.identity_token_balance.data_contract_id) + .find(|c| { + c.contract.id() == self.identity_token_info.data_contract.contract.id() + }) .expect("Data contract not found") .contract .clone(); + let group_info; + if self.group_action_id.is_some() { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }); + } else { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }); + } + action = AppAction::BackendTask(BackendTask::TokenTask(TokenTask::ResumeTokens { actor_identity: self.identity.clone(), data_contract, - token_position: self.identity_token_balance.token_position, + token_position: self.identity_token_info.token_position, signing_key: self.selected_key.clone().expect("No key selected"), public_note: self.public_note.clone(), + group_info, })); } @@ -208,11 +304,21 @@ impl ResumeTokensScreen { ui.add_space(50.0); ui.heading("🎉"); - ui.heading("Resumed Successfully!"); + if self.group_action_id.is_some() { + ui.label("Group Resume Signing Successful."); + } else { + ui.heading("Resume Successful."); + } ui.add_space(20.0); - if ui.button("Back to Tokens").clicked() { + let button_text; + if self.group_action_id.is_some() { + button_text = "Back to Group Actions"; + } else { + button_text = "Back to Tokens"; + } + if ui.button(button_text).clicked() { action = AppAction::PopScreenAndRefresh; } }); @@ -250,19 +356,32 @@ impl ScreenLike for ResumeTokensScreen { } fn ui(&mut self, ctx: &Context) -> AppAction { - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Tokens", AppAction::GoToMainScreen), - ( - &self.identity_token_balance.token_alias, - AppAction::PopScreen, - ), - ("Resume", AppAction::None), - ], - vec![], - ); + let mut action; + + // Build a top panel + if self.group_action_id.is_some() { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Group Actions", AppAction::PopScreen), + ("Resume", AppAction::None), + ], + vec![], + ); + } else { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Tokens", AppAction::GoToMainScreen), + (&self.identity_token_info.token_alias, AppAction::PopScreen), + ("Resume", AppAction::None), + ], + vec![], + ); + } // Left panel action |= add_left_panel( @@ -371,17 +490,29 @@ impl ScreenLike for ResumeTokensScreen { ) .changed() { - self.public_note = Some(txt); + self.public_note = if txt.len() > 0 { Some(txt) } else { None }; } }); - ui.add_space(10.0); - let button = egui::Button::new(RichText::new("Resume").color(Color32::WHITE)) - .fill(Color32::from_rgb(0, 128, 0)) - .corner_radius(3.0); + let button_text = render_group_action_text( + ui, + &self.group, + &self.identity_token_info, + "Resume", + &self.group_action_id, + ); + + // Resume button + if self.app_context.developer_mode || !button_text.contains("Test") { + ui.add_space(10.0); + let button = + egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() { + self.show_confirmation_popup = true; + } } // If user pressed "Resume," show popup diff --git a/src/ui/tokens/tokens_screen.rs b/src/ui/tokens/tokens_screen.rs index 6311dfa97..f4c3cac79 100644 --- a/src/ui/tokens/tokens_screen.rs +++ b/src/ui/tokens/tokens_screen.rs @@ -1768,8 +1768,6 @@ impl TokensScreen { // self.sort_vec(&mut detail_list); // } - // This is basically your old `render_table_my_token_balances` logic, but - // limited to just the single token. // Allocate space for refreshing indicator let refreshing_height = 33.0; let mut max_scroll_height = if let RefreshingStatus::Refreshing(_) = self.refreshing_status @@ -1965,69 +1963,111 @@ impl TokensScreen { ui.close_menu(); } if ui.button("Burn").clicked() { - action = AppAction::AddScreen( - Screen::BurnTokensScreen( - BurnTokensScreen::new( - itb.clone(), - &self.app_context, - ), - ), - ); + match IdentityTokenInfo::try_from_identity_token_balance_with_lookup(itb, &self.app_context) { + Ok(info) => { + action = AppAction::AddScreen( + Screen::BurnTokensScreen( + BurnTokensScreen::new( + info, + &self.app_context, + ), + ), + ); + } + Err(e) => { + self.set_error_message(Some(e)); + } + }; ui.close_menu(); } if ui.button("Freeze").clicked() { - action = AppAction::AddScreen( - Screen::FreezeTokensScreen( - FreezeTokensScreen::new( - itb.clone(), - &self.app_context, - ), - ), - ); + match IdentityTokenInfo::try_from_identity_token_balance_with_lookup(itb, &self.app_context) { + Ok(info) => { + action = AppAction::AddScreen( + Screen::FreezeTokensScreen( + FreezeTokensScreen::new( + info, + &self.app_context, + ), + ), + ); + } + Err(e) => { + self.set_error_message(Some(e)); + } + }; ui.close_menu(); } if ui.button("Destroy").clicked() { - action = AppAction::AddScreen( - Screen::DestroyFrozenFundsScreen( - DestroyFrozenFundsScreen::new( - itb.clone(), - &self.app_context, - ), - ), - ); + match IdentityTokenInfo::try_from_identity_token_balance_with_lookup(itb, &self.app_context) { + Ok(info) => { + action = AppAction::AddScreen( + Screen::DestroyFrozenFundsScreen( + DestroyFrozenFundsScreen::new( + info, + &self.app_context, + ), + ), + ); + } + Err(e) => { + self.set_error_message(Some(e)); + } + }; ui.close_menu(); } if ui.button("Unfreeze").clicked() { - action = AppAction::AddScreen( - Screen::UnfreezeTokensScreen( - UnfreezeTokensScreen::new( - itb.clone(), - &self.app_context, - ), - ), - ); + match IdentityTokenInfo::try_from_identity_token_balance_with_lookup(itb, &self.app_context) { + Ok(info) => { + action = AppAction::AddScreen( + Screen::UnfreezeTokensScreen( + UnfreezeTokensScreen::new( + info, + &self.app_context, + ), + ), + ); + } + Err(e) => { + self.set_error_message(Some(e)); + } + }; ui.close_menu(); } if ui.button("Pause").clicked() { - action = AppAction::AddScreen( - Screen::PauseTokensScreen( - PauseTokensScreen::new( - itb.clone(), - &self.app_context, - ), - ), - ); + match IdentityTokenInfo::try_from_identity_token_balance_with_lookup(itb, &self.app_context) { + Ok(info) => { + action = AppAction::AddScreen( + Screen::PauseTokensScreen( + PauseTokensScreen::new( + info, + &self.app_context, + ), + ), + ); + } + Err(e) => { + self.set_error_message(Some(e)); + } + }; ui.close_menu(); } if ui.button("Resume").clicked() { - action = AppAction::AddScreen( - Screen::ResumeTokensScreen( - ResumeTokensScreen::new( - itb.clone(), - &self.app_context, - ), - ), - ); + match IdentityTokenInfo::try_from_identity_token_balance_with_lookup(itb, &self.app_context) { + Ok(info) => { + action = AppAction::AddScreen( + Screen::ResumeTokensScreen( + ResumeTokensScreen::new( + info, + &self.app_context, + ), + ), + ); + } + Err(e) => { + self.set_error_message(Some(e)); + } + }; ui.close_menu(); } if ui.button("View Claims").clicked() { @@ -2042,14 +2082,21 @@ impl TokensScreen { ui.close_menu(); } if ui.button("Update Config").clicked() { - action = AppAction::AddScreen( - Screen::UpdateTokenConfigScreen( - UpdateTokenConfigScreen::new( - itb.clone(), - &self.app_context, - ), - ), - ); + match IdentityTokenInfo::try_from_identity_token_balance_with_lookup(itb, &self.app_context) { + Ok(info) => { + action = AppAction::AddScreen( + Screen::UpdateTokenConfigScreen( + UpdateTokenConfigScreen::new( + info, + &self.app_context, + ), + ), + ); + } + Err(e) => { + self.set_error_message(Some(e)); + } + }; ui.close_menu(); } // Purchase @@ -4935,9 +4982,13 @@ Emits tokens in fixed amounts for specific intervals. self.display_message("Added token", MessageType::Success); - AppAction::BackendTask(BackendTask::TokenTask(TokenTask::SaveTokenLocally( - token_info, - ))) + AppAction::BackendTasks( + vec![ + BackendTask::TokenTask(TokenTask::SaveTokenLocally(token_info)), + BackendTask::TokenTask(TokenTask::QueryMyTokenBalances), + ], + BackendTasksExecutionMode::Sequential, + ) } fn goto_next_search_page(&mut self) -> AppAction { diff --git a/src/ui/tokens/transfer_tokens_screen.rs b/src/ui/tokens/transfer_tokens_screen.rs index 57a51ef33..78dfdfc2b 100644 --- a/src/ui/tokens/transfer_tokens_screen.rs +++ b/src/ui/tokens/transfer_tokens_screen.rs @@ -44,8 +44,8 @@ pub struct TransferTokensScreen { selected_friend_index: Option, selected_key: Option, pub public_note: Option, - receiver_identity_id: String, - amount: String, + pub receiver_identity_id: String, + pub amount: String, transfer_tokens_status: TransferTokensStatus, max_amount: u64, pub app_context: Arc, diff --git a/src/ui/tokens/unfreeze_tokens_screen.rs b/src/ui/tokens/unfreeze_tokens_screen.rs index 8feedc942..97ec305e6 100644 --- a/src/ui/tokens/unfreeze_tokens_screen.rs +++ b/src/ui/tokens/unfreeze_tokens_screen.rs @@ -3,10 +3,17 @@ use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use dash_sdk::dpp::data_contract::group::Group; +use dash_sdk::dpp::data_contract::GroupContractPosition; +use dash_sdk::dpp::group::GroupStateTransitionInfoStatus; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use eframe::egui::{self, Color32, Context, Ui}; use egui::RichText; +use dash_sdk::dpp::group::GroupStateTransitionInfo; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; @@ -22,12 +29,13 @@ use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::helpers::render_group_action_text; use crate::ui::identities::get_selected_wallet; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::{MessageType, Screen, ScreenLike}; -use super::tokens_screen::IdentityTokenBalance; +use super::tokens_screen::IdentityTokenInfo; /// The states for the unfreeze flow #[derive(PartialEq)] @@ -41,12 +49,15 @@ pub enum UnfreezeTokensStatus { /// A screen that allows unfreezing a previously frozen identity’s tokens for a specific contract pub struct UnfreezeTokensScreen { pub identity: QualifiedIdentity, - pub identity_token_balance: IdentityTokenBalance, + pub identity_token_info: IdentityTokenInfo, selected_key: Option, public_note: Option, - /// Identity that’s currently frozen and we want to unfreeze - unfreeze_identity_id: String, + group: Option<(GroupContractPosition, Group)>, + pub group_action_id: Option, + + /// The identity we want to freeze + pub unfreeze_identity_id: String, status: UnfreezeTokensStatus, error_message: Option, @@ -64,37 +75,103 @@ pub struct UnfreezeTokensScreen { } impl UnfreezeTokensScreen { - pub fn new( - identity_token_balance: IdentityTokenBalance, - app_context: &Arc, - ) -> Self { - let identity = app_context - .load_local_qualified_identities() - .unwrap_or_default() - .into_iter() - .find(|id| id.identity.id() == identity_token_balance.identity_id) - .expect("No local qualified identity found for this token’s identity"); - - let identity_clone = identity.identity.clone(); - let possible_key = identity_clone.get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); + pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + let possible_key = identity_token_info + .identity + .identity + .get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ) + .cloned(); let mut error_message = None; - let selected_wallet = - get_selected_wallet(&identity, None, possible_key.clone(), &mut error_message); + + let group = match identity_token_info + .token_config + .unfreeze_rules() + .authorized_to_make_change_action_takers() + { + AuthorizedActionTakers::NoOne => { + error_message = Some("Burning is not allowed on this token".to_string()); + None + } + AuthorizedActionTakers::ContractOwner => { + if identity_token_info.data_contract.contract.owner_id() + != &identity_token_info.identity.identity.id() + { + error_message = Some( + "You are not allowed to burn this token. Only the contract owner is." + .to_string(), + ); + } + None + } + AuthorizedActionTakers::Identity(identifier) => { + if identifier != &identity_token_info.identity.identity.id() { + error_message = Some("You are not allowed to burn this token".to_string()); + } + None + } + AuthorizedActionTakers::MainGroup => { + match identity_token_info.token_config.main_control_group() { + None => { + error_message = Some( + "Invalid contract: No main control group, though one should exist" + .to_string(), + ); + None + } + Some(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(group_pos) + { + Ok(group) => Some((group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + } + } + AuthorizedActionTakers::Group(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(*group_pos) + { + Ok(group) => Some((*group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + }; + + // Attempt to get an unlocked wallet reference + let selected_wallet = get_selected_wallet( + &identity_token_info.identity, + None, + possible_key.as_ref(), + &mut error_message, + ); Self { - identity, - identity_token_balance, - selected_key: possible_key.cloned(), + identity: identity_token_info.identity.clone(), + identity_token_info, + selected_key: possible_key, + group, + group_action_id: None, public_note: None, unfreeze_identity_id: String::new(), status: UnfreezeTokensStatus::NotStarted, - error_message: None, + error_message, app_context: app_context.clone(), show_confirmation_popup: false, selected_wallet, @@ -209,20 +286,40 @@ impl UnfreezeTokensScreen { .get_contracts(None, None) .expect("Contracts not loaded") .iter() - .find(|c| c.contract.id() == self.identity_token_balance.data_contract_id) + .find(|c| { + c.contract.id() == self.identity_token_info.data_contract.contract.id() + }) .expect("Data contract not found") .contract .clone(); + let group_info; + if self.group_action_id.is_some() { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }); + } else { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }); + } + // Dispatch to backend action = AppAction::BackendTask(BackendTask::TokenTask(TokenTask::UnfreezeTokens { actor_identity: self.identity.clone(), data_contract, - token_position: self.identity_token_balance.token_position, + token_position: self.identity_token_info.token_position, signing_key: self.selected_key.clone().expect("No key selected"), public_note: self.public_note.clone(), unfreeze_identity: unfreeze_id, + group_info, })); } @@ -244,11 +341,21 @@ impl UnfreezeTokensScreen { ui.add_space(50.0); ui.heading("🎉"); - ui.heading("Unfroze Successfully!"); + if self.group_action_id.is_some() { + ui.label("Group Unfreeze Signing Successful."); + } else { + ui.heading("Unfreeze Successful."); + } ui.add_space(20.0); - if ui.button("Back to Tokens").clicked() { + let button_text; + if self.group_action_id.is_some() { + button_text = "Back to Group Actions"; + } else { + button_text = "Back to Tokens"; + } + if ui.button(button_text).clicked() { action = AppAction::PopScreenAndRefresh; } }); @@ -286,19 +393,32 @@ impl ScreenLike for UnfreezeTokensScreen { } fn ui(&mut self, ctx: &Context) -> AppAction { - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Tokens", AppAction::GoToMainScreen), - ( - &self.identity_token_balance.token_alias, - AppAction::PopScreen, - ), - ("Unfreeze", AppAction::None), - ], - vec![], - ); + let mut action; + + // Build a top panel + if self.group_action_id.is_some() { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Group Actions", AppAction::PopScreen), + ("Unfreeze", AppAction::None), + ], + vec![], + ); + } else { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Tokens", AppAction::GoToMainScreen), + (&self.identity_token_info.token_alias, AppAction::PopScreen), + ("Unfreeze", AppAction::None), + ], + vec![], + ); + } // Left panel action |= add_left_panel( @@ -398,7 +518,18 @@ impl ScreenLike for UnfreezeTokensScreen { // 2) Identity to unfreeze ui.heading("2. Enter the identity ID to unfreeze"); ui.add_space(5.0); - self.render_unfreeze_identity_input(ui); + if self.group_action_id.is_some() { + ui.label( + "You are signing an existing group Unfreeze so you are not allowed to choose the identity.", + ); + ui.add_space(5.0); + ui.label(format!( + "Identity: {}", + self.unfreeze_identity_id + )); + } else { + self.render_unfreeze_identity_input(ui); + } ui.add_space(10.0); ui.separator(); @@ -418,18 +549,33 @@ impl ScreenLike for UnfreezeTokensScreen { ) .changed() { - self.public_note = Some(txt); + self.public_note = if txt.len() > 0 { + Some(txt) + } else { + None + }; } }); - ui.add_space(10.0); + + let button_text = render_group_action_text( + ui, + &self.group, + &self.identity_token_info, + "Unfreeze", + &self.group_action_id, + ); // Unfreeze button - let button = egui::Button::new(RichText::new("Unfreeze").color(Color32::WHITE)) - .fill(Color32::from_rgb(0, 128, 128)) - .corner_radius(3.0); + if self.app_context.developer_mode || !button_text.contains("Test") { + ui.add_space(10.0); + let button = + egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() { + self.show_confirmation_popup = true; + } } // If user pressed "Unfreeze," show popup diff --git a/src/ui/tokens/update_token_config.rs b/src/ui/tokens/update_token_config.rs index ed7cfa983..66ff44249 100644 --- a/src/ui/tokens/update_token_config.rs +++ b/src/ui/tokens/update_token_config.rs @@ -1,3 +1,27 @@ +use std::collections::{BTreeMap, HashSet}; +use std::sync::{Arc, RwLock}; + +use chrono::{DateTime, Utc}; +use dash_sdk::dpp::balances::credits::TokenAmount; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; +use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; +use dash_sdk::dpp::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; +use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; +use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; +use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; +use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; +use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; +use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use dash_sdk::dpp::data_contract::group::Group; +use dash_sdk::dpp::data_contract::GroupContractPosition; +use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; +use eframe::egui::{self, Color32, Context, Ui}; +use egui::RichText; + +use super::tokens_screen::IdentityTokenInfo; use crate::app::AppAction; use crate::backend_task::tokens::TokenTask; use crate::backend_task::BackendTask; @@ -9,34 +33,16 @@ use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::helpers::render_group_action_text; use crate::ui::identities::get_selected_wallet; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::{MessageType, Screen, ScreenLike}; -use chrono::{DateTime, Utc}; -use dash_sdk::dpp::balances::credits::TokenAmount; -use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; -use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; -use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; -use dash_sdk::dpp::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; -use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; -use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; -use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; -use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; -use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; -use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::platform::{Identifier, IdentityPublicKey}; -use egui::{Color32, Ui}; -use egui::{Context, RichText}; -use std::sync::RwLock; -use std::collections::{BTreeMap, HashSet}; -use std::sync::Arc; - -use super::tokens_screen::IdentityTokenBalance; #[derive(Debug, Clone, PartialEq)] pub enum UpdateTokenConfigStatus { @@ -45,15 +51,17 @@ pub enum UpdateTokenConfigStatus { } pub struct UpdateTokenConfigScreen { - pub identity_token_balance: IdentityTokenBalance, + pub identity_token_info: IdentityTokenInfo, data_contract_option: Option, backend_message: Option<(String, MessageType, DateTime)>, update_status: UpdateTokenConfigStatus, pub app_context: Arc, - change_item: TokenConfigurationChangeItem, + pub change_item: TokenConfigurationChangeItem, signing_key: Option, identity: QualifiedIdentity, public_note: Option, + group: Option<(GroupContractPosition, Group)>, + pub group_action_id: Option, // Input state fields pub authorized_identity_input: Option, @@ -66,44 +74,105 @@ pub struct UpdateTokenConfigScreen { } impl UpdateTokenConfigScreen { - pub fn new( - identity_token_balance: IdentityTokenBalance, - app_context: &Arc, - ) -> Self { - // Find the local qualified identity that corresponds to `identity_token_balance.identity_id` - let identity = app_context - .load_local_qualified_identities() - .unwrap_or_default() - .into_iter() - .find(|id| id.identity.id() == identity_token_balance.identity_id) - .expect("No local qualified identity found matching the token's identity"); - - // Grab a default key if possible - let identity_clone = identity.identity.clone(); - let possible_key = identity_clone.get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); + pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + let possible_key = identity_token_info + .identity + .identity + .get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ) + .cloned(); let mut error_message = None; - let selected_wallet = - get_selected_wallet(&identity, None, possible_key.clone(), &mut error_message); + + let group = match identity_token_info + .token_config + .manual_minting_rules() + .authorized_to_make_change_action_takers() + { + AuthorizedActionTakers::NoOne => { + error_message = Some("Minting is not allowed on this token".to_string()); + None + } + AuthorizedActionTakers::ContractOwner => { + if identity_token_info.data_contract.contract.owner_id() + != &identity_token_info.identity.identity.id() + { + error_message = Some( + "You are not allowed to mint this token. Only the contract owner is." + .to_string(), + ); + } + None + } + AuthorizedActionTakers::Identity(identifier) => { + if identifier != &identity_token_info.identity.identity.id() { + error_message = Some("You are not allowed to mint this token".to_string()); + } + None + } + AuthorizedActionTakers::MainGroup => { + match identity_token_info.token_config.main_control_group() { + None => { + error_message = Some( + "Invalid contract: No main control group, though one should exist" + .to_string(), + ); + None + } + Some(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(group_pos) + { + Ok(group) => Some((group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + } + } + AuthorizedActionTakers::Group(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(*group_pos) + { + Ok(group) => Some((*group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + }; + + // Attempt to get an unlocked wallet reference + let selected_wallet = get_selected_wallet( + &identity_token_info.identity, + None, + possible_key.as_ref(), + &mut error_message, + ); let data_contract_option = app_context - .get_contract_by_id(&identity_token_balance.data_contract_id) + .get_contract_by_id(&identity_token_info.data_contract.contract.id()) .unwrap_or_default(); Self { - identity_token_balance: identity_token_balance.clone(), + identity_token_info: identity_token_info.clone(), data_contract_option, backend_message: None, update_status: UpdateTokenConfigStatus::NotUpdating, app_context: app_context.clone(), change_item: TokenConfigurationChangeItem::TokenConfigurationNoChange, - signing_key: possible_key.cloned(), - identity, + signing_key: possible_key, public_note: None, authorized_identity_input: None, @@ -113,6 +182,10 @@ impl UpdateTokenConfigScreen { wallet_password: String::new(), show_password: false, error_message, + + identity: identity_token_info.identity, + group, + group_action_id: None, } } @@ -121,6 +194,9 @@ impl UpdateTokenConfigScreen { ui.heading("2. Select the item to update"); ui.add_space(10.0); + if self.group_action_id.is_some() { + ui.label("You are signing an existing group action. Make sure you construct the exact same item as the one in the group action, details of which can be found on the previous screen."); + } let item = &mut self.change_item; @@ -512,27 +588,56 @@ impl UpdateTokenConfigScreen { let mut txt = self.public_note.clone().unwrap_or_default(); if ui .text_edit_singleline(&mut txt) - .on_hover_text("A note to go with the transaction that can be seen by the public") + .on_hover_text("A note about the transaction that can be seen by the public.") .changed() { - self.public_note = Some(txt); + self.public_note = if txt.len() > 0 { Some(txt) } else { None }; } }); - ui.add_space(20.0); + let button_text = render_group_action_text( + ui, + &self.group, + &self.identity_token_info, + "Update Config", + &self.group_action_id, + ); - let button = egui::Button::new(RichText::new("Broadcast Update").color(Color32::WHITE)) + let button = egui::Button::new(RichText::new(&button_text).color(Color32::WHITE)) .fill(Color32::from_rgb(0, 128, 255)) .frame(true) .corner_radius(3.0); - if ui.add(button).clicked() { - self.update_status = UpdateTokenConfigStatus::Updating(Utc::now()); - action = AppAction::BackendTask(BackendTask::TokenTask(TokenTask::UpdateTokenConfig { - identity_token_balance: self.identity_token_balance.clone(), - change_item: self.change_item.clone(), - signing_key: self.signing_key.clone().expect("Signing key must be set"), - public_note: self.public_note.clone(), - })); + + if self.app_context.developer_mode || !button_text.contains("Test") { + ui.add_space(20.0); + if ui.add(button).clicked() { + let group_info; + if self.group_action_id.is_some() { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }); + } else { + group_info = self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }); + } + + self.update_status = UpdateTokenConfigStatus::Updating(Utc::now()); + action = + AppAction::BackendTask(BackendTask::TokenTask(TokenTask::UpdateTokenConfig { + identity_token_info: self.identity_token_info.clone(), + change_item: self.change_item.clone(), + signing_key: self.signing_key.clone().expect("Signing key must be set"), + public_note: self.public_note.clone(), + group_info, + })); + } } action @@ -672,11 +777,21 @@ impl UpdateTokenConfigScreen { ui.add_space(50.0); ui.heading("🎉"); - ui.heading(format!("{}", self.backend_message.as_ref().unwrap().0)); + if self.group_action_id.is_some() { + ui.label("Group Update Signing Successful."); + } else { + ui.heading(format!("{}", self.backend_message.as_ref().unwrap().0)); + } ui.add_space(20.0); - if ui.button("Back to Tokens").clicked() { + let button_text; + if self.group_action_id.is_some() { + button_text = "Back to Group Actions"; + } else { + button_text = "Back to Tokens"; + } + if ui.button(button_text).clicked() { action = AppAction::PopScreenAndRefresh; } }); @@ -763,20 +878,32 @@ impl ScreenLike for UpdateTokenConfigScreen { } fn ui(&mut self, ctx: &Context) -> AppAction { - // Top panel - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Tokens", AppAction::GoToMainScreen), - ( - &self.identity_token_balance.token_alias, - AppAction::PopScreen, - ), - ("Update Config", AppAction::None), - ], - vec![], - ); + let mut action; + + // Build a top panel + if self.group_action_id.is_some() { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Group Actions", AppAction::PopScreen), + ("Update Token Config", AppAction::None), + ], + vec![], + ); + } else { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Tokens", AppAction::GoToMainScreen), + (&self.identity_token_info.token_alias, AppAction::PopScreen), + ("Update Token Config", AppAction::None), + ], + vec![], + ); + } // Left panel action |= add_left_panel(