diff --git a/.github/workflows/nym-wallet-publish-macos.yml b/.github/workflows/nym-wallet-publish-macos.yml index 23044a79f6..c491db6451 100644 --- a/.github/workflows/nym-wallet-publish-macos.yml +++ b/.github/workflows/nym-wallet-publish-macos.yml @@ -66,6 +66,7 @@ jobs: APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + ADMIN_ADDRESS: ${{ secrets.WALLET_ADMIN_ADDRESS }} run: yarn && yarn build - name: Upload to release based on tag name diff --git a/.github/workflows/nym-wallet-publish-ubuntu.yml b/.github/workflows/nym-wallet-publish-ubuntu.yml index 36893ba388..b319f5eb2a 100644 --- a/.github/workflows/nym-wallet-publish-ubuntu.yml +++ b/.github/workflows/nym-wallet-publish-ubuntu.yml @@ -44,6 +44,7 @@ jobs: env: TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + ADMIN_ADDRESS: ${{ secrets.WALLET_ADMIN_ADDRESS }} - name: Upload to release based on tag name uses: softprops/action-gh-release@v1 with: diff --git a/.github/workflows/nym-wallet-publish-windows10.yml b/.github/workflows/nym-wallet-publish-windows10.yml index 132cb71df6..ef67b73718 100644 --- a/.github/workflows/nym-wallet-publish-windows10.yml +++ b/.github/workflows/nym-wallet-publish-windows10.yml @@ -65,6 +65,7 @@ jobs: WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + ADMIN_ADDRESS: ${{ secrets.WALLET_ADMIN_ADDRESS }} run: yarn build - name: Upload to release based on tag name diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5127b964..1bde8b06ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - wallet: compound and claim reward endpoints for operators and delegators ([#1302]) - wallet: require password to switch accounts - wallet: the wallet backend learned how to keep track of validator name, either hardcoded or by querying the status endpoint. +- wallet: new delegation and rewards UI +- wallet: show version in nav bar +- wallet: contract admin route put back - network-statistics: a new mixnet service that aggregates and exposes anonymized data about mixnet services ([#1328]) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index d800d74f15..48c921e8d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,9 +218,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64ct" -version = "1.5.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179" +checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" [[package]] name = "binascii" @@ -3204,6 +3204,32 @@ dependencies = [ "version-checker", ] +[[package]] +name = "nym-types" +version = "1.0.0" +dependencies = [ + "coconut-interface", + "config", + "cosmrs", + "cosmwasm-std", + "eyre", + "itertools", + "log", + "mixnet-contract-common", + "reqwest", + "schemars", + "serde", + "serde_json", + "strum", + "tempfile", + "thiserror", + "ts-rs", + "url", + "validator-client", + "vesting-contract", + "vesting-contract-common", +] + [[package]] name = "nym-validator-api" version = "1.0.1" @@ -3251,6 +3277,7 @@ dependencies = [ "tokio", "tokio-stream", "topology", + "ts-rs", "url", "validator-api-requests", "validator-client", @@ -3258,6 +3285,24 @@ dependencies = [ "version-checker", ] +[[package]] +name = "nym-wallet-types" +version = "1.0.0" +dependencies = [ + "config", + "cosmrs", + "cosmwasm-std", + "mixnet-contract-common", + "nym-types", + "serde", + "serde_json", + "strum", + "ts-rs", + "validator-client", + "vesting-contract", + "vesting-contract-common", +] + [[package]] name = "nymcoconut" version = "0.5.0" @@ -5399,6 +5444,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "1.0.0" @@ -6025,6 +6092,21 @@ dependencies = [ "ts-rs-macros", ] +[[package]] +name = "ts-rs-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "mixnet-contract-common", + "nym-types", + "nym-wallet-types", + "ts-rs", + "validator-api-requests", + "validator-client", + "vesting-contract-common", + "walkdir", +] + [[package]] name = "ts-rs-macros" version = "6.1.2" diff --git a/Cargo.toml b/Cargo.toml index 4ffc4cbdd2..7f6f46a9f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ members = [ "common/socks5/requests", "common/task", "common/topology", + "common/types", "common/wasm-utils", "explorer-api", "gateway", @@ -67,6 +68,7 @@ members = [ "service-providers/network-statistics", "validator-api", "validator-api/validator-api-requests", + "tools/ts-rs-cli" ] default-members = [ @@ -79,4 +81,4 @@ default-members = [ "explorer-api", ] -exclude = ["explorer", "contracts", "tokenomics-py", "clients/webassembly"] +exclude = ["explorer", "contracts", "tokenomics-py", "clients/webassembly", "nym-wallet"] diff --git a/Makefile b/Makefile index 3d87c659b5..bef0f9e874 100644 --- a/Makefile +++ b/Makefile @@ -54,3 +54,7 @@ fmt-wallet: wasm: RUSTFLAGS='-C link-arg=-s' cargo build --manifest-path contracts/Cargo.toml --release --target wasm32-unknown-unknown + +generate-typescript: + cd tools/ts-rs-cli && cargo run && cd ../.. + yarn types:lint:fix diff --git a/assets/token/token-dark-testnet.svg b/assets/token/token-dark-testnet.svg new file mode 100644 index 0000000000..1487796dfd --- /dev/null +++ b/assets/token/token-dark-testnet.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/token/token-dark.svg b/assets/token/token-dark.svg index 9c5b8401e8..958a3a39ba 100644 --- a/assets/token/token-dark.svg +++ b/assets/token/token-dark.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/token/token-light-testnet.svg b/assets/token/token-light-testnet.svg new file mode 100644 index 0000000000..230b52e4e4 --- /dev/null +++ b/assets/token/token-light-testnet.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/token/token-light.svg b/assets/token/token-light.svg index 946a23b1df..fd0e1897bc 100644 --- a/assets/token/token-light.svg +++ b/assets/token/token-light.svg @@ -1,4 +1,4 @@ - + diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 10322feda7..ff2a6714a1 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -58,3 +58,5 @@ nymd-client = [ "itertools", "cosmwasm-std", ] +generate-ts = [] + diff --git a/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs index 5bf493fbc9..7ddf3ef1dd 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs @@ -4,11 +4,11 @@ use crate::nymd::error::NymdError; use cosmrs::tendermint::abci; use itertools::Itertools; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; // it seems that currently validators just emit stringified events (which are also returned as part of deliverTx response) // as theirs logs -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct Log { #[serde(default)] // weird thing is that the first msg_index seems to always be undefined on the raw logs diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 3c79bf1f6c..8210879768 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -289,7 +289,17 @@ impl NymdClient { where C: CosmWasmClient + Sync, { - Ok(self.client.get_block(None).await?.block.header.time) + self.get_block_timestamp(None).await + } + + pub async fn get_block_timestamp( + &self, + height: Option, + ) -> Result + where + C: CosmWasmClient + Sync, + { + Ok(self.client.get_block(height).await?.block.header.time) } pub async fn get_current_block_height(&self) -> Result diff --git a/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs index ea20cb6654..f79197a9f5 100644 --- a/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs @@ -183,6 +183,7 @@ impl VestingQueryClient for NymdClient { .map(Into::into) } + /// Returns the total amount of delegated tokens that have vested async fn delegated_vesting( &self, vesting_account_address: &str, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index 3cc065da7e..33439678c5 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -18,12 +18,13 @@ fixed = { version = "1.1", features = ["serde"] } az = "1.1" log = "0.4.14" time = { version = "0.3.6", features = ["parsing", "formatting"] } +ts-rs = "6.1.2" contracts-common = { path = "../contracts-common" } [dev-dependencies] time = { version = "0.3.5", features = ["serde", "macros"] } -ts-rs = "6.1.2" [features] default = [] +generate-ts = [] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index 22800f23dd..291f15f12c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -8,11 +8,6 @@ use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::fmt::Display; -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr( - test, - ts(export, export_to = "../../../nym-wallet/src/types/rust/gateway.ts") -)] #[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)] pub struct Gateway { pub host: String, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index b8622e0f11..32b8611eff 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -14,13 +14,10 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use std::cmp::Ordering; use std::fmt::Display; -#[cfg_attr(test, derive(ts_rs::TS))] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( - test, - ts( - export, - export_to = "../../../nym-wallet/src/types/rust/rewardedsetnodestatus.ts" - ) + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/RewardedSetNodeStatus.ts") )] #[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] pub enum RewardedSetNodeStatus { @@ -109,11 +106,6 @@ impl PendingUndelegate { } } -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr( - test, - ts(export, export_to = "../../../nym-wallet/src/types/rust/mixnode.ts") -)] #[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)] pub struct MixNode { pub host: String, diff --git a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml index ede76f5998..2452f6eeb1 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml @@ -12,6 +12,7 @@ serde = { version = "1.0", features = ["derive"] } schemars = "0.8" cw-storage-plus = "0.13.4" config = { path = "../../config" } - -[dev-dependencies] ts-rs = "6.1.2" + +[features] +generate-ts = [] diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs index 4c5fd864e9..b35b574819 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs @@ -14,10 +14,10 @@ pub fn one_ucoin() -> Coin { Coin::new(1, DENOM) } -#[cfg_attr(test, derive(ts_rs::TS))] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( - test, - ts(export, export_to = "../../../nym-wallet/src/types/rust/period.ts") + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Period.ts") )] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone, JsonSchema)] pub enum Period { diff --git a/common/types/Cargo.toml b/common/types/Cargo.toml new file mode 100644 index 0000000000..7c692d86fd --- /dev/null +++ b/common/types/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "nym-types" +version = "1.0.0" +description = "Nym common types" +authors = ["Nym Technologies SA"] +edition = "2021" +rust-version = "1.58" + +[dependencies] +eyre = "0.6.5" +log = "0.4" +itertools = "0.10" +reqwest = "0.11.9" +schemars = "0.8" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +strum = { version = "0.23", features = ["derive"] } +thiserror = "1.0" +url = "2.2" +ts-rs = "6.1.2" + +cosmwasm-std = "1.0.0-beta8" +cosmrs = "0.7.0" + +validator-client = { path = "../../common/client-libs/validator-client", features = [ + "nymd-client", +] } +mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } +vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" } +config = { path = "../../common/config" } +coconut-interface = { path = "../../common/coconut-interface" } +# Used for Type conversion, can be extracted but its a lot of work +vesting-contract = { path = "../../contracts/vesting" } + +[dev-dependencies] +tempfile = "3.3.0" + +[features] +default = [] +generate-ts = [] diff --git a/common/types/src/account.rs b/common/types/src/account.rs new file mode 100644 index 0000000000..00ec29a703 --- /dev/null +++ b/common/types/src/account.rs @@ -0,0 +1,58 @@ +use crate::currency::{CurrencyDenom, MajorCurrencyAmount}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Account.ts") +)] +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct Account { + pub contract_address: String, + pub client_address: String, + pub denom: CurrencyDenom, +} + +impl Account { + pub fn new(contract_address: String, client_address: String, denom: CurrencyDenom) -> Self { + Account { + contract_address, + client_address, + denom, + } + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/AccountWithMnemonic.ts") +)] +#[derive(Serialize, Deserialize)] +pub struct AccountWithMnemonic { + pub account: Account, + pub mnemonic: String, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/AccountEntry.ts") +)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AccountEntry { + pub id: String, + pub address: String, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Balance.ts") +)] +#[derive(Serialize, Deserialize)] +pub struct Balance { + pub amount: MajorCurrencyAmount, + pub printable_balance: String, +} diff --git a/common/types/src/currency.rs b/common/types/src/currency.rs new file mode 100644 index 0000000000..568b9ed3b8 --- /dev/null +++ b/common/types/src/currency.rs @@ -0,0 +1,515 @@ +use crate::error::TypesError; +use cosmrs::Denom as CosmosDenom; +use cosmwasm_std::Coin as CosmWasmCoin; +use cosmwasm_std::{Decimal, Uint128}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +use std::fmt::{Display, Formatter}; +use std::ops::{Add, Mul}; +use std::str::FromStr; +use strum::{Display, EnumString, EnumVariantNames}; +use validator_client::nymd::{Coin, CosmosCoin}; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/CurrencyDenom.ts") +)] +#[cfg_attr(feature = "generate-ts", ts(rename_all = "UPPERCASE"))] +#[derive( + Display, + Serialize, + Deserialize, + Clone, + Debug, + EnumString, + EnumVariantNames, + PartialEq, + JsonSchema, +)] +#[serde(rename_all = "UPPERCASE")] +#[strum(serialize_all = "UPPERCASE")] +// TODO: this shouldn't be an enum... +pub enum CurrencyDenom { + #[strum(ascii_case_insensitive)] + Nym, + #[strum(ascii_case_insensitive)] + Nymt, + #[strum(ascii_case_insensitive)] + Nyx, + #[strum(ascii_case_insensitive)] + Nyxt, +} + +impl CurrencyDenom { + pub fn parse(value: &str) -> Result { + let mut denom = value.to_string(); + if denom.starts_with('u') { + denom = denom[1..].to_string(); + } + match CurrencyDenom::from_str(&denom) { + Ok(res) => Ok(res), + Err(_e) => Err(TypesError::InvalidDenom(value.to_string())), + } + } +} + +impl TryFrom for CurrencyDenom { + type Error = TypesError; + + fn try_from(value: CosmosDenom) -> Result { + CurrencyDenom::parse(&value.to_string()) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/CurrencyStringMajorAmount.ts") +)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct MajorAmountString(String); // see https://github.com/Aleph-Alpha/ts-rs/issues/51 for exporting type aliases + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Currency.ts") +)] +// #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct MajorCurrencyAmount { + // temporarly going back to original impl to speed up merge + pub amount: MajorAmountString, + pub denom: CurrencyDenom, + // // temporary... + // #[cfg_attr(feature = "generate-ts", ts(skip))] + // pub coin: Coin, +} + +// impl JsonSchema for MajorCurrencyAmount { +// fn schema_name() -> String { +// todo!() +// } +// +// fn json_schema(gen: &mut SchemaGenerator) -> Schema { +// todo!() +// } +// } + +// tries to semi-replicate cosmos-sdk's DecCoin for being able to handle tokens with decimal amounts +// https://github.com/cosmos/cosmos-sdk/blob/v0.45.4/types/dec_coin.go +pub struct DecCoin { + // +} + +impl MajorCurrencyAmount { + pub fn new(amount: &str, denom: CurrencyDenom) -> MajorCurrencyAmount { + MajorCurrencyAmount { + amount: MajorAmountString(amount.to_string()), + denom, + } + } + + pub fn zero(denom: &CurrencyDenom) -> MajorCurrencyAmount { + MajorCurrencyAmount::new("0", denom.clone()) + } + // + // pub fn from_cosmrs_coin(coin: &CosmosCoin) -> Result { + // MajorCurrencyAmount::from_cosmrs_decimal_and_denom(coin.amount, coin.denom.to_string()) + // } + // + // pub fn from_minor_uint128_and_denom( + // amount_minor: Uint128, + // denom_minor: &str, + // ) -> Result { + // MajorCurrencyAmount::from_minor_decimal_and_denom( + // Decimal::from_atomics(amount_minor, 0)?, + // denom_minor, + // ) + // } + // + // pub fn from_minor_decimal_and_denom( + // amount_minor: Decimal, + // denom_minor: &str, + // ) -> Result { + // if !(denom_minor.starts_with('u') || denom_minor.starts_with('U')) { + // return Err(TypesError::InvalidDenom(denom_minor.to_string())); + // } + // let major = amount_minor / Uint128::from(1_000_000u64); + // if let Ok(denom) = CurrencyDenom::from_str(&denom_minor[1..].to_string()) { + // return Ok(MajorCurrencyAmount { + // amount: MajorAmountString(major.to_string()), + // denom, + // }); + // } + // Err(TypesError::InvalidDenom(denom_minor.to_string())) + // } + // pub fn from_decimal_and_denom( + // amount: Decimal, + // denom: String, + // ) -> Result { + // if denom.starts_with('u') || denom.starts_with('U') { + // return MajorCurrencyAmount::from_minor_decimal_and_denom(amount, &denom); + // } + // if let Ok(denom) = CurrencyDenom::from_str(denom.as_str()) { + // return Ok(MajorCurrencyAmount { + // amount: MajorAmountString(amount.to_string()), + // denom, + // }); + // } + // Err(TypesError::InvalidDenom(denom)) + // } + // pub fn from_cosmrs_decimal_and_denom( + // amount: CosmosDecimal, + // denom: String, + // ) -> Result { + // if denom.starts_with('u') || denom.starts_with('U') { + // return match Decimal::from_str(&amount.to_string()) { + // Ok(amount) => MajorCurrencyAmount::from_minor_decimal_and_denom(amount, &denom), + // Err(_e) => Err(TypesError::InvalidAmount(amount.to_string())), + // }; + // } + // + // if let Ok(denom) = CurrencyDenom::from_str(denom.as_str()) { + // return Ok(MajorCurrencyAmount { + // amount: MajorAmountString(amount.to_string()), + // denom, + // }); + // } + // Err(TypesError::InvalidDenom(denom)) + // } + // + // pub fn into_cosmos_coin(self) -> CosmosCoin { + // self.coin.into() + // } + // + // pub fn to_minor_uint128(&self) -> Result { + // if self.amount.0.contains('.') { + // // has a decimal point (Cosmos assumes "." is the decimal separator) + // let parts = self.amount.0.split('.'); + // let str = parts.collect_vec(); + // if str.is_empty() || str.len() > 2 { + // return Err(TypesError::InvalidAmount("Amount is invalid".to_string())); + // } + // if str.len() == 2 { + // // has a decimal, so check decimal places first + // if str[1].len() > 6 { + // return Err(TypesError::InvalidDenom( + // "Amount is invalid, only 6 decimal places of precision are allowed" + // .to_string(), + // )); + // } + // + // // so multiple whole part by 1e6 and add decimal part + // let whole_part = Uint128::from_str(str[0])? * Uint128::from(1_000_000u64); + // + // // TODO: has Rust got anything that deals with fixed point values, or parsing from format strings? Leading zeroes are causing issues + // return match format!("0.{}", str[1]).parse::() { + // Ok(decimal_part_float) => { + // // this makes an assumption that 6 decimal places of f64 can never lose precision + // let truncated = (decimal_part_float * 1_000_000.).trunc() as u32; + // let decimal_part = Uint128::from(truncated); + // let sum = whole_part + decimal_part; + // Ok(sum) + // } + // Err(_e) => Err(TypesError::InvalidAmount( + // "Amount decimal part is invalid".to_string(), + // )), + // }; + // } + // } + // + // let major = Uint128::from_str(&self.amount.0)?; + // let scaled = major * Uint128::new(1_000_000u128); + // Ok(scaled) + // } + + // pub fn denom_to_string(&self) -> String { + // self.denom.to_string() + // } +} + +impl Display for MajorCurrencyAmount { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {}", self.amount.0, self.denom) + } +} + +// TODO: cleanup after merge +impl From for MajorCurrencyAmount { + fn from(c: CosmosCoin) -> Self { + MajorCurrencyAmount::from(Coin::from(c)) + } +} + +impl From for MajorCurrencyAmount { + fn from(c: CosmWasmCoin) -> Self { + MajorCurrencyAmount::from(Coin::from(c)) + } +} + +impl From for MajorCurrencyAmount { + fn from(coin: Coin) -> Self { + // current assumption: MajorCurrencyAmount is represented as decimal with 6 decimal points + // unwrap is fine as we haven't exceeded decimal range since our coins are at max 1B in value + // (this is a weak assumption, but for solving this merge conflict it's good enough temporary workaround) + let amount = Decimal::from_atomics(coin.amount, 6).unwrap(); + MajorCurrencyAmount { + amount: MajorAmountString(amount.to_string()), + denom: CurrencyDenom::parse(&coin.denom).expect("this will go away after the merge..."), + } + } +} + +// temporary... +impl From for CosmosCoin { + fn from(c: MajorCurrencyAmount) -> CosmosCoin { + let c: Coin = c.into(); + c.into() + } +} + +impl From for CosmWasmCoin { + fn from(c: MajorCurrencyAmount) -> CosmWasmCoin { + let c: Coin = c.into(); + c.into() + } +} + +impl From for Coin { + fn from(c: MajorCurrencyAmount) -> Coin { + let decimal: Decimal = c + .amount + .0 + .parse() + .expect("stringified amount should have been a valid decimal"); + + // again, temporary + let exp = Uint128::new(1000000); + let val = decimal.mul(exp); + + // again, terrible assumption for denom, but it works temporarily... + Coin { + amount: val.u128(), + denom: format!("u{}", c.denom).to_lowercase(), + } + } +} + +impl Add for MajorCurrencyAmount { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + // again, temporary workaround to help with merge + (Coin::from(self).try_add(&Coin::from(rhs))) + .expect("provided coins had different denoms") + .into() + } +} + +#[cfg(test)] +mod test { + use super::*; + use cosmrs::Coin as CosmosCoin; + use cosmrs::Decimal as CosmosDecimal; + use cosmrs::Denom as CosmosDenom; + use cosmwasm_std::Coin as CosmWasmCoin; + use cosmwasm_std::Decimal as CosmWasmDecimal; + use serde_json::json; + use std::convert::TryFrom; + use std::str::FromStr; + use std::string::ToString; + + #[test] + fn json_to_major_currency_amount() { + let nym = json!({ + "amount": "1", + "denom": "NYM" + }); + let nymt = json!({ + "amount": "1", + "denom": "NYMT" + }); + + let test_nym_amount = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); + let test_nymt_amount = MajorCurrencyAmount::new("1", CurrencyDenom::Nymt); + + let nym_amount = serde_json::from_value::(nym).unwrap(); + let nymt_amount = serde_json::from_value::(nymt).unwrap(); + + assert_eq!(nym_amount, test_nym_amount); + assert_eq!(nymt_amount, test_nymt_amount); + } + + #[test] + fn minor_amount_json_to_major_currency_amount() { + let one_micro_nym = json!({ + "amount": "0.000001", + "denom": "NYM" + }); + + let expected_nym_amount = MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym); + let actual_nym_amount = + serde_json::from_value::(one_micro_nym).unwrap(); + + assert_eq!(expected_nym_amount, actual_nym_amount); + } + + #[test] + fn denom_from_str() { + assert_eq!(CurrencyDenom::from_str("nym").unwrap(), CurrencyDenom::Nym); + assert_eq!( + CurrencyDenom::from_str("nymt").unwrap(), + CurrencyDenom::Nymt + ); + assert_eq!(CurrencyDenom::from_str("NYM").unwrap(), CurrencyDenom::Nym); + assert_eq!( + CurrencyDenom::from_str("NYMT").unwrap(), + CurrencyDenom::Nymt + ); + assert_eq!(CurrencyDenom::from_str("NyM").unwrap(), CurrencyDenom::Nym); + assert_eq!( + CurrencyDenom::from_str("NYmt").unwrap(), + CurrencyDenom::Nymt + ); + + assert!(matches!( + CurrencyDenom::from_str("foo").unwrap_err(), + strum::ParseError::VariantNotFound, + )); + + // denominations must all be major + assert!(matches!( + CurrencyDenom::from_str("unym").unwrap_err(), + strum::ParseError::VariantNotFound, + )); + assert!(matches!( + CurrencyDenom::from_str("unymt").unwrap_err(), + strum::ParseError::VariantNotFound, + )); + } + + #[test] + fn to_string() { + assert_eq!( + MajorCurrencyAmount::new("1", CurrencyDenom::Nym).to_string(), + "1 NYM" + ); + assert_eq!( + MajorCurrencyAmount::new("1", CurrencyDenom::Nymt).to_string(), + "1 NYMT" + ); + assert_eq!( + MajorCurrencyAmount::new("1000000000000", CurrencyDenom::Nym).to_string(), + "1000000000000 NYM" + ); + } + + #[test] + fn minor_coin_to_major_currency() { + let cosmos_coin = CosmosCoin { + amount: CosmosDecimal::from(1u64), + denom: CosmosDenom::from_str("unym").unwrap(), + }; + let c = MajorCurrencyAmount::from(cosmos_coin); + assert_eq!(c, MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym)); + } + + #[test] + fn minor_cosmwasm_coin_to_major_currency() { + let coin = CosmWasmCoin { + amount: Uint128::from(1u64), + denom: "unym".to_string(), + }; + println!( + "from_atomics = {}", + CosmWasmDecimal::from_atomics(coin.amount.clone(), 6) + .unwrap() + .to_string() + ); + let c: MajorCurrencyAmount = coin.into(); + assert_eq!(c, MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym)); + } + + #[test] + fn minor_cosmwasm_coin_to_major_currency_2() { + let coin = CosmWasmCoin { + amount: Uint128::from(1_000_000u64), + denom: "unym".to_string(), + }; + println!( + "from_atomics = {:?}", + CosmWasmDecimal::from_atomics(coin.amount.clone(), 6) + .unwrap() + .to_string() + ); + let c: MajorCurrencyAmount = coin.into(); + assert_eq!(c, MajorCurrencyAmount::new("1", CurrencyDenom::Nym)); + } + + #[test] + fn major_currency_to_minor_cosmos_coin() { + let expected_cosmos_coin = CosmosCoin { + amount: CosmosDecimal::from(1u64), + denom: CosmosDenom::from_str("unym").unwrap(), + }; + let c = MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym); + let minor_cosmos_coin = c.into(); + assert_eq!(expected_cosmos_coin, minor_cosmos_coin); + assert_eq!("unym", minor_cosmos_coin.denom.to_string()); + } + + #[test] + fn major_currency_to_minor_cosmos_coin_2() { + let expected_cosmos_coin = CosmosCoin { + amount: CosmosDecimal::from(1000000u64), + denom: CosmosDenom::from_str("unym").unwrap(), + }; + let c = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); + let minor_cosmos_coin = c.into(); + assert_eq!(expected_cosmos_coin, minor_cosmos_coin); + assert_eq!("unym", minor_cosmos_coin.denom.to_string()); + } + + #[test] + fn minor_cosmos_coin_to_major_currency_string() { + // check minor cosmos coin is converted to major value + let cosmos_coin = CosmosCoin { + amount: CosmosDecimal::from(1u64), + denom: CosmosDenom::from_str("unym").unwrap(), + }; + let c = MajorCurrencyAmount::from(cosmos_coin); + assert_eq!(c.to_string(), "0.000001 NYM"); + } + + #[test] + fn denom_to_string() { + let c = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); + let denom = c.denom.to_string(); + assert_eq!(denom, "NYM".to_string()); + } + + fn amounts() -> Vec<&'static str> { + vec![ + "1", + "10", + "100", + "1000", + "10000", + "100000", + "10000000", + "100000000", + "1000000000", + "10000000000", + "100000000000", + "1000000000000", + "10000000000000", + "100000000000000", + "1000000000000000", + "10000000000000000", + "100000000000000000", + "1000000000000000000", + ] + } +} diff --git a/common/types/src/delegation.rs b/common/types/src/delegation.rs new file mode 100644 index 0000000000..8b273529dd --- /dev/null +++ b/common/types/src/delegation.rs @@ -0,0 +1,233 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use log::error; +use mixnet_contract_common::mixnode::DelegationEvent as ContractDelegationEvent; +use mixnet_contract_common::mixnode::PendingUndelegate as ContractPendingUndelegate; +use mixnet_contract_common::Delegation as MixnetContractDelegation; + +use crate::currency::MajorCurrencyAmount; +use crate::error::TypesError; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Delegation.ts") +)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +pub struct Delegation { + pub owner: String, + pub node_identity: String, + pub amount: MajorCurrencyAmount, + pub block_height: u64, + pub proxy: Option, // proxy address used to delegate the funds on behalf of anouther address +} + +impl TryFrom for Delegation { + type Error = TypesError; + + fn try_from(value: MixnetContractDelegation) -> Result { + let MixnetContractDelegation { + owner, + node_identity, + amount, + block_height, + proxy, + } = value; + + let amount: MajorCurrencyAmount = amount.into(); + + Ok(Delegation { + owner: owner.into_string(), + node_identity, + amount, + block_height, + proxy: proxy.map(|p| p.into_string()), + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DelegationRecord.ts") +)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +pub struct DelegationRecord { + pub amount: MajorCurrencyAmount, + pub block_height: u64, + pub delegated_on_iso_datetime: String, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DelegationWithEverything.ts") +)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +pub struct DelegationWithEverything { + pub owner: String, + pub node_identity: String, + pub amount: MajorCurrencyAmount, + pub total_delegation: Option, + pub pledge_amount: Option, + pub block_height: u64, + pub delegated_on_iso_datetime: String, + pub profit_margin_percent: Option, + pub avg_uptime_percent: Option, + pub stake_saturation: Option, + pub proxy: Option, + pub accumulated_rewards: Option, + pub pending_events: Vec, + pub history: Vec, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DelegationResult.ts") +)] +#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Debug)] +pub struct DelegationResult { + source_address: String, + target_address: String, + amount: Option, +} + +impl DelegationResult { + pub fn new( + source_address: &str, + target_address: &str, + amount: Option, + ) -> DelegationResult { + DelegationResult { + source_address: source_address.to_string(), + target_address: target_address.to_string(), + amount, + } + } +} + +impl TryFrom for DelegationResult { + type Error = TypesError; + + fn try_from(delegation: MixnetContractDelegation) -> Result { + let amount: MajorCurrencyAmount = delegation.amount.clone().into(); + Ok(DelegationResult { + source_address: delegation.owner().to_string(), + target_address: delegation.node_identity(), + amount: Some(amount), + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DelegationEventKind.ts") +)] +#[derive(Clone, Deserialize, Serialize, PartialEq, JsonSchema, Debug)] +pub enum DelegationEventKind { + Delegate, + Undelegate, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DelegationEvent.ts") +)] +#[derive(Clone, Deserialize, Serialize, PartialEq, JsonSchema, Debug)] +pub struct DelegationEvent { + pub kind: DelegationEventKind, + pub node_identity: String, + pub address: String, + pub amount: Option, + pub block_height: u64, +} + +impl TryFrom for DelegationEvent { + type Error = TypesError; + + fn try_from(event: ContractDelegationEvent) -> Result { + match event { + ContractDelegationEvent::Delegate(delegation) => { + let amount: MajorCurrencyAmount = delegation.amount.into(); + Ok(DelegationEvent { + kind: DelegationEventKind::Delegate, + block_height: delegation.block_height, + address: delegation.owner.into_string(), + node_identity: delegation.node_identity, + amount: Some(amount), + }) + } + ContractDelegationEvent::Undelegate(pending_undelegate) => Ok(DelegationEvent { + kind: DelegationEventKind::Undelegate, + block_height: pending_undelegate.block_height(), + address: pending_undelegate.delegate().into_string(), + node_identity: pending_undelegate.mix_identity(), + amount: None, + }), + } + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PendingUndelegate.ts") +)] +#[derive(Deserialize, Serialize, PartialEq, JsonSchema, Clone, Debug)] +pub struct PendingUndelegate { + mix_identity: String, + delegate: String, + proxy: Option, + block_height: u64, +} + +impl From for PendingUndelegate { + fn from(pending_undelegate: ContractPendingUndelegate) -> Self { + PendingUndelegate { + mix_identity: pending_undelegate.mix_identity(), + delegate: pending_undelegate.delegate().to_string(), + proxy: pending_undelegate.proxy().map(|p| p.to_string()), + block_height: pending_undelegate.block_height(), + } + } +} + +pub fn from_contract_delegation_events( + events: Vec, +) -> Result, TypesError> { + let (events, errors): (Vec<_>, Vec<_>) = events + .into_iter() + .map(|delegation_event| delegation_event.try_into()) + .partition(Result::is_ok); + + if errors.is_empty() { + let events = events + .into_iter() + .filter_map(|e| e.ok()) + .collect::>(); + return Ok(events); + } + let errors = errors + .into_iter() + .filter_map(|e| e.err()) + .collect::>(); + + error!("Failed to convert delegations: {:?}", errors); + Err(TypesError::DelegationsInvalid) +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DelegationSummaryResponse.ts") +)] +#[derive(Deserialize, Serialize)] +pub struct DelegationsSummaryResponse { + pub delegations: Vec, + pub total_delegations: MajorCurrencyAmount, + pub total_rewards: MajorCurrencyAmount, +} diff --git a/common/types/src/error.rs b/common/types/src/error.rs new file mode 100644 index 0000000000..d1903d643f --- /dev/null +++ b/common/types/src/error.rs @@ -0,0 +1,83 @@ +use serde::{Serialize, Serializer}; +use std::io; +use thiserror::Error; +use validator_client::validator_api::error::ValidatorAPIError; +use validator_client::{nymd::error::NymdError, ValidatorClientError}; + +#[derive(Error, Debug)] +pub enum TypesError { + #[error("{source}")] + NymdError { + #[from] + source: NymdError, + }, + #[error("{source}")] + CosmwasmStd { + #[from] + source: cosmwasm_std::StdError, + }, + #[error("{source}")] + ErrorReport { + #[from] + source: eyre::Report, + }, + #[error("{source}")] + ValidatorApiError { + #[from] + source: ValidatorAPIError, + }, + #[error("{source}")] + IOError { + #[from] + source: io::Error, + }, + #[error("{source}")] + SerdeJsonError { + #[from] + source: serde_json::Error, + }, + #[error("{source}")] + MalformedUrlProvided { + #[from] + source: url::ParseError, + }, + #[error("{source}")] + ReqwestError { + #[from] + source: reqwest::Error, + }, + #[error("{source}")] + DecimalRangeExceeded { + #[from] + source: cosmwasm_std::DecimalRangeExceeded, + }, + #[error("{0} is not a valid amount string")] + InvalidAmount(String), + #[error("{0} is not a valid denomination string")] + InvalidDenom(String), + #[error("Mixnode not found")] + MixnodeNotFound(), + #[error("Gateway bond is not valid")] + InvalidGatewayBond(), + #[error("Invalid delegations")] + DelegationsInvalid, +} + +impl Serialize for TypesError { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_str(self) + } +} + +impl From for TypesError { + fn from(e: ValidatorClientError) -> Self { + match e { + ValidatorClientError::ValidatorAPIError { source } => source.into(), + ValidatorClientError::MalformedUrlProvided(e) => e.into(), + ValidatorClientError::NymdError(e) => e.into(), + } + } +} diff --git a/common/types/src/fees.rs b/common/types/src/fees.rs new file mode 100644 index 0000000000..5037646c31 --- /dev/null +++ b/common/types/src/fees.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +use validator_client::nymd::Fee; + +use crate::currency::MajorCurrencyAmount; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/FeeDetails.ts") +)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FeeDetails { + // expected to be used by the wallet in order to display detailed fee information to the user + pub amount: Option, + #[cfg_attr(feature = "generate-ts", ts(skip))] + pub fee: Fee, +} diff --git a/common/types/src/gas.rs b/common/types/src/gas.rs new file mode 100644 index 0000000000..8ffb475f4e --- /dev/null +++ b/common/types/src/gas.rs @@ -0,0 +1,94 @@ +use crate::currency::MajorCurrencyAmount; +use crate::error::TypesError; +use cosmrs::tx::Gas as CosmrsGas; +use serde::{Deserialize, Serialize}; +use validator_client::nymd::cosmwasm_client::types::GasInfo as ValidatorClientGasInfo; +use validator_client::nymd::GasPrice; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Gas.ts") +)] +#[derive(Deserialize, Serialize, Clone)] +pub struct Gas { + /// units of gas used + pub gas_units: u64, + // + // /// gas units converted to fee as major coin amount + // pub amount: MajorCurrencyAmount, +} + +impl Gas { + pub fn from_cosmrs_gas(value: CosmrsGas, _denom_minor: &str) -> Result { + Ok(Gas { + gas_units: value.value(), + }) + + // // TODO: use simulator struct to do conversion to fee + // let value_u128 = Uint128::from(value.value()); + // let amount = Decimal::new(value_u128) * Decimal::from_str("0.0025")?; + // Ok(Gas { + // gas_units: value.value(), + // amount: MajorCurrencyAmount::from_minor_decimal_and_denom(amount, denom_minor)?, + // }) + } + pub fn from_u64(value: u64, _denom_minor: &str) -> Result { + Ok(Gas { gas_units: value }) + // todo!() + // // TODO: use simulator struct to do conversion to fee + // let value_u128 = Uint128::from(value); + // let amount = Decimal::new(value_u128) * Decimal::from_str("0.0025")?; + // Ok(Gas { + // gas_units: value, + // amount: MajorCurrencyAmount::from_minor_decimal_and_denom(amount, denom_minor)?, + // }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/GasInfo.ts") +)] +#[derive(Deserialize, Serialize)] +pub struct GasInfo { + /// GasWanted is the maximum units of work we allow this tx to perform. + pub gas_wanted: u64, + + /// GasUsed is the amount of gas actually consumed. + pub gas_used: u64, + + /// gas units converted to fee as major coin amount + pub fee: MajorCurrencyAmount, +} + +impl GasInfo { + pub fn from_validator_client_gas_info( + value: ValidatorClientGasInfo, + denom_minor: &str, + ) -> Result { + // terrible workaround, but I don't want to break the current flow (just yet) + let gas_price = GasPrice::new_with_default_price(denom_minor)?; + let fee = (&gas_price) * value.gas_used; + Ok(GasInfo { + gas_wanted: value.gas_wanted.value(), + gas_used: value.gas_used.value(), + fee: fee.into(), + }) + } + pub fn from_u64( + gas_wanted: u64, + gas_used: u64, + denom_minor: &str, + ) -> Result { + // terrible workaround, but I don't want to break the current flow (just yet) + let gas_price = GasPrice::new_with_default_price(denom_minor)?; + let fee = (&gas_price) * CosmrsGas::from(gas_used); + Ok(GasInfo { + gas_wanted, + gas_used, + fee: fee.into(), + }) + } +} diff --git a/common/types/src/gateway.rs b/common/types/src/gateway.rs new file mode 100644 index 0000000000..d45d0f8274 --- /dev/null +++ b/common/types/src/gateway.rs @@ -0,0 +1,100 @@ +use crate::currency::MajorCurrencyAmount; +use crate::error::TypesError; +use mixnet_contract_common::{ + Gateway as MixnetContractGateway, GatewayBond as MixnetContractGatewayBond, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Gateway.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)] +pub struct Gateway { + pub host: String, + pub mix_port: u16, + pub clients_port: u16, + pub location: String, + pub sphinx_key: String, + /// Base58 encoded ed25519 EdDSA public key of the gateway used to derive shared keys with clients + pub identity_key: String, + pub version: String, +} + +impl From for Gateway { + fn from(value: MixnetContractGateway) -> Self { + let MixnetContractGateway { + host, + mix_port, + clients_port, + location, + sphinx_key, + identity_key, + version, + } = value; + + Gateway { + host, + mix_port, + clients_port, + location, + sphinx_key, + identity_key, + version, + } + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/GatewayBond.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct GatewayBond { + pub pledge_amount: MajorCurrencyAmount, + pub owner: String, + pub block_height: u64, + pub gateway: Gateway, + pub proxy: Option, +} + +impl GatewayBond { + pub fn from_mixnet_contract_gateway_bond( + bond: Option, + ) -> Result, TypesError> { + match bond { + Some(bond) => { + let bond: GatewayBond = bond.try_into()?; + Ok(Some(bond)) + } + None => Ok(None), + } + } +} + +impl TryFrom for GatewayBond { + type Error = TypesError; + + fn try_from(value: MixnetContractGatewayBond) -> Result { + let MixnetContractGatewayBond { + pledge_amount, + owner, + block_height, + gateway, + proxy, + } = value; + + let pledge_amount: MajorCurrencyAmount = pledge_amount.into(); + + Ok(GatewayBond { + pledge_amount, + owner: owner.into_string(), + block_height, + gateway: gateway.into(), + proxy: proxy.map(|p| p.into_string()), + }) + } +} diff --git a/common/types/src/lib.rs b/common/types/src/lib.rs new file mode 100644 index 0000000000..8a64ecac07 --- /dev/null +++ b/common/types/src/lib.rs @@ -0,0 +1,10 @@ +pub mod account; +pub mod currency; +pub mod delegation; +pub mod error; +pub mod fees; +pub mod gas; +pub mod gateway; +pub mod mixnode; +pub mod transaction; +pub mod vesting; diff --git a/common/types/src/mixnode.rs b/common/types/src/mixnode.rs new file mode 100644 index 0000000000..85c65be18d --- /dev/null +++ b/common/types/src/mixnode.rs @@ -0,0 +1,124 @@ +use crate::currency::MajorCurrencyAmount; +use crate::error::TypesError; +use mixnet_contract_common::{ + Coin as CosmWasmCoin, MixNode as MixnetContractMixNode, + MixNodeBond as MixnetContractMixNodeBond, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Mixnode.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)] +pub struct MixNode { + pub host: String, + pub mix_port: u16, + pub verloc_port: u16, + pub http_api_port: u16, + pub sphinx_key: String, + /// Base58 encoded ed25519 EdDSA public key. + pub identity_key: String, + pub version: String, + pub profit_margin_percent: u8, +} + +impl From for MixNode { + fn from(value: MixnetContractMixNode) -> Self { + let MixnetContractMixNode { + host, + mix_port, + verloc_port, + http_api_port, + sphinx_key, + identity_key, + version, + profit_margin_percent, + } = value; + + Self { + host, + mix_port, + verloc_port, + http_api_port, + sphinx_key, + identity_key, + version, + profit_margin_percent, + } + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/MixNodeBond.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixNodeBond { + pub pledge_amount: MajorCurrencyAmount, + pub total_delegation: MajorCurrencyAmount, + pub owner: String, + pub layer: String, + pub block_height: u64, + pub mix_node: MixNode, + pub proxy: Option, + pub accumulated_rewards: Option, +} + +impl MixNodeBond { + pub fn from_mixnet_contract_mixnode_bond( + bond: Option, + ) -> Result, TypesError> { + match bond { + Some(bond) => { + let bond: MixNodeBond = bond.try_into()?; + Ok(Some(bond)) + } + None => Ok(None), + } + } +} + +impl TryFrom for MixNodeBond { + type Error = TypesError; + + fn try_from(value: MixnetContractMixNodeBond) -> Result { + let MixnetContractMixNodeBond { + pledge_amount, + total_delegation, + owner, + layer, + block_height, + mix_node, + proxy, + accumulated_rewards, + } = value; + + if pledge_amount.denom != total_delegation.denom { + return Err(TypesError::InvalidDenom( + "The pledge and delegation denominations do not match".to_string(), + )); + } + + let denom = total_delegation.denom.clone(); + + let pledge_amount: MajorCurrencyAmount = pledge_amount.into(); + let total_delegation: MajorCurrencyAmount = total_delegation.into(); + let accumulated_rewards: Option = + accumulated_rewards.map(|r| CosmWasmCoin::new(r.u128(), denom).into()); + + Ok(MixNodeBond { + pledge_amount, + total_delegation, + owner: owner.into_string(), + layer: layer.into(), + block_height, + mix_node: mix_node.into(), + proxy: proxy.map(|p| p.into_string()), + accumulated_rewards, + }) + } +} diff --git a/common/types/src/transaction.rs b/common/types/src/transaction.rs new file mode 100644 index 0000000000..956e8cfc92 --- /dev/null +++ b/common/types/src/transaction.rs @@ -0,0 +1,126 @@ +use crate::currency::MajorCurrencyAmount; +use crate::error::TypesError; +use crate::gas::GasInfo; +use serde::{Deserialize, Serialize}; +use validator_client::nymd::cosmwasm_client::types::ExecuteResult; +use validator_client::nymd::TxResponse; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/SendTxResult.ts") +)] +#[derive(Deserialize, Serialize, Debug)] +pub struct SendTxResult { + pub block_height: u64, + pub code: u32, + pub details: TransactionDetails, + pub gas_used: u64, + pub gas_wanted: u64, + pub tx_hash: String, + // pub fee: MajorCurrencyAmount, +} + +impl SendTxResult { + pub fn new( + t: TxResponse, + details: TransactionDetails, + _denom_minor: &str, + ) -> Result { + Ok(SendTxResult { + block_height: t.height.value(), + code: t.tx_result.code.value(), + details, + gas_used: t.tx_result.gas_used.value(), + gas_wanted: t.tx_result.gas_wanted.value(), + tx_hash: t.hash.to_string(), + // that is completely wrong: fee is what you told the validator to use beforehand + // fee: MajorCurrencyAmount::from_decimal_and_denom( + // Decimal::new(Uint128::from(t.tx_result.gas_used.value())), + // denom_minor.to_string(), + // )?, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/TransactionDetails.ts") +)] +#[derive(Deserialize, Serialize, Debug)] +pub struct TransactionDetails { + pub amount: MajorCurrencyAmount, + pub from_address: String, + pub to_address: String, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/TransactionExecuteResult.ts") +)] +#[derive(Deserialize, Serialize)] +pub struct TransactionExecuteResult { + pub logs_json: String, + pub data_json: String, + pub transaction_hash: String, + pub gas_info: GasInfo, + pub fee: MajorCurrencyAmount, +} + +impl TransactionExecuteResult { + pub fn from_execute_result( + value: ExecuteResult, + denom_minor: &str, + ) -> Result { + let gas_info = GasInfo::from_validator_client_gas_info(value.gas_info, denom_minor)?; + let fee = gas_info.fee.clone(); + Ok(TransactionExecuteResult { + gas_info, + transaction_hash: value.transaction_hash.to_string(), + data_json: ::serde_json::to_string_pretty(&value.data)?, + logs_json: ::serde_json::to_string_pretty(&value.logs)?, + fee, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/RpcTransactionResponse.ts") +)] +#[derive(Deserialize, Serialize)] +pub struct RpcTransactionResponse { + pub index: u32, + pub tx_result_json: String, + pub block_height: u64, + pub transaction_hash: String, + pub gas_info: GasInfo, + // pub fee: MajorCurrencyAmount, +} + +impl RpcTransactionResponse { + pub fn from_tx_response( + value: &TxResponse, + denom_minor: &str, + ) -> Result { + Ok(RpcTransactionResponse { + index: value.index, + gas_info: GasInfo::from_u64( + value.tx_result.gas_wanted.value(), + value.tx_result.gas_used.value(), + denom_minor, + )?, + transaction_hash: value.hash.to_string(), + tx_result_json: ::serde_json::to_string_pretty(&value.tx_result)?, + block_height: value.height.value(), + // wrong + // fee: MajorCurrencyAmount::from_decimal_and_denom( + // Decimal::new(Uint128::from(value.tx_result.gas_used.value())), + // denom_minor.to_string(), + // )?, + }) + } +} diff --git a/common/types/src/vesting.rs b/common/types/src/vesting.rs new file mode 100644 index 0000000000..c20b0845e0 --- /dev/null +++ b/common/types/src/vesting.rs @@ -0,0 +1,114 @@ +use crate::currency::MajorCurrencyAmount; +use crate::error::TypesError; +use serde::{Deserialize, Serialize}; +use vesting_contract::vesting::Account as VestingAccount; +use vesting_contract::vesting::VestingPeriod as VestingVestingPeriod; +use vesting_contract_common::OriginalVestingResponse as VestingOriginalVestingResponse; +use vesting_contract_common::PledgeData as VestingPledgeData; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PledgeData.ts") +)] +#[derive(Serialize, Deserialize, Debug)] +pub struct PledgeData { + pub amount: MajorCurrencyAmount, + pub block_time: u64, +} + +impl TryFrom for PledgeData { + type Error = TypesError; + + fn try_from(data: VestingPledgeData) -> Result { + let amount: MajorCurrencyAmount = data.amount().into(); + Ok(Self { + amount, + block_time: data.block_time().seconds(), + }) + } +} + +impl PledgeData { + pub fn and_then(data: VestingPledgeData) -> Option { + data.try_into().ok() + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/OriginalVestingResponse.ts") +)] +#[derive(Serialize, Deserialize, Debug)] +pub struct OriginalVestingResponse { + amount: MajorCurrencyAmount, + number_of_periods: usize, + period_duration: u64, +} + +impl TryFrom for OriginalVestingResponse { + type Error = TypesError; + + fn try_from(data: VestingOriginalVestingResponse) -> Result { + let amount = data.amount().into(); + Ok(Self { + amount, + number_of_periods: data.number_of_periods(), + period_duration: data.period_duration(), + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/VestingAccountInfo.ts") +)] +#[derive(Serialize, Deserialize, Debug)] +pub struct VestingAccountInfo { + owner_address: String, + staking_address: Option, + start_time: u64, + periods: Vec, + amount: MajorCurrencyAmount, +} + +impl TryFrom for VestingAccountInfo { + type Error = TypesError; + + fn try_from(account: VestingAccount) -> Result { + let mut periods = Vec::new(); + for period in account.periods() { + periods.push(period.into()); + } + let amount: MajorCurrencyAmount = account.coin().into(); + Ok(Self { + owner_address: account.owner_address().to_string(), + staking_address: account.staking_address().map(|a| a.to_string()), + start_time: account.start_time().seconds(), + periods, + amount, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/VestingPeriod.ts") +)] +#[derive(Serialize, Deserialize, Debug)] +pub struct VestingPeriod { + start_time: u64, + period_seconds: u64, +} + +impl From for VestingPeriod { + fn from(period: VestingVestingPeriod) -> Self { + Self { + start_time: period.start_time, + period_seconds: period.period_seconds, + } + } +} diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 1fca87b909..32e9865982 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "aes" version = "0.7.5" @@ -1056,6 +1062,7 @@ dependencies = [ "serde_repr", "thiserror", "time 0.3.6", + "ts-rs", ] [[package]] @@ -1627,6 +1634,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.30" @@ -1699,6 +1715,29 @@ dependencies = [ "serde", ] +[[package]] +name = "ts-rs" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc59f479df54269b400dd95bc3b7e81623b3e4b9c70c8ca7125ab8341eafa64e" +dependencies = [ + "thiserror", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f807fdb3151fee75df7485b901a89624358cd07a67a8fb1a5831bf5a07681ff" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn", + "termcolor", +] + [[package]] name = "typenum" version = "1.15.0" @@ -1809,6 +1848,7 @@ dependencies = [ "mixnet-contract-common", "schemars", "serde", + "ts-rs", ] [[package]] @@ -1893,6 +1933,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/lerna.json b/lerna.json index 36261f8aa7..cc6884af07 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,7 @@ { "packages": [ - "ts-packages/*" + "ts-packages/*", + "nym-wallet" ], "version": "0.0.0" } diff --git a/nym-wallet/.env.sample b/nym-wallet/.env.sample index cc3febee19..f0c41db0c6 100644 --- a/nym-wallet/.env.sample +++ b/nym-wallet/.env.sample @@ -1 +1 @@ -ADMIN_ADDRESS= \ No newline at end of file +ADMIN_ADDRESS={"MAINNET":[],"SANDBOX":[],"QA":["n1c8te4wlc25re97qw5nh8urtpek494zqx30lza2","n177krl498arsfupd2p9097kwfmuf07lkj6crmj9"]} \ No newline at end of file diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 98b497e2da..c6795aa890 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -2785,6 +2785,7 @@ dependencies = [ "serde_repr", "thiserror", "time 0.3.7", + "ts-rs", ] [[package]] @@ -2997,6 +2998,49 @@ dependencies = [ "libc", ] +[[package]] +name = "nym-types" +version = "1.0.0" +dependencies = [ + "coconut-interface", + "config", + "cosmrs", + "cosmwasm-std", + "eyre", + "itertools", + "log", + "mixnet-contract-common", + "reqwest", + "schemars", + "serde", + "serde_json", + "strum 0.23.0", + "thiserror", + "ts-rs", + "url", + "validator-client", + "vesting-contract", + "vesting-contract-common", +] + +[[package]] +name = "nym-wallet-types" +version = "1.0.0" +dependencies = [ + "config", + "cosmrs", + "cosmwasm-std", + "mixnet-contract-common", + "nym-types", + "serde", + "serde_json", + "strum 0.23.0", + "ts-rs", + "validator-client", + "vesting-contract", + "vesting-contract-common", +] + [[package]] name = "nym_wallet" version = "1.0.4" @@ -3009,6 +3053,7 @@ dependencies = [ "coconut-interface", "colored", "config", + "cosmrs", "cosmwasm-std", "dirs", "dotenv", @@ -3017,6 +3062,8 @@ dependencies = [ "itertools", "log", "mixnet-contract-common", + "nym-types", + "nym-wallet-types", "once_cell", "pretty_env_logger", "rand 0.6.5", @@ -5450,6 +5497,7 @@ dependencies = [ "mixnet-contract-common", "schemars", "serde", + "ts-rs", ] [[package]] @@ -5541,6 +5589,7 @@ dependencies = [ "mixnet-contract-common", "schemars", "serde", + "ts-rs", ] [[package]] diff --git a/nym-wallet/Cargo.toml b/nym-wallet/Cargo.toml index 03978d332c..bb834e3d59 100644 --- a/nym-wallet/Cargo.toml +++ b/nym-wallet/Cargo.toml @@ -4,4 +4,5 @@ resolver = "2" members = [ "src-tauri", "wallet-recovery-cli", + "nym-wallet-types" ] diff --git a/nym-wallet/nym-wallet-types/Cargo.toml b/nym-wallet/nym-wallet-types/Cargo.toml new file mode 100644 index 0000000000..103d32b5e6 --- /dev/null +++ b/nym-wallet/nym-wallet-types/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "nym-wallet-types" +version = "1.0.0" +edition = "2021" +rust-version = "1.58" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +strum = { version = "0.23", features = ["derive"] } +ts-rs = "6.1.2" + +cosmwasm-std = "1.0.0-beta8" +cosmrs = "0.7.0" + +config = { path = "../../common/config" } +mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } +validator-client = { path = "../../common/client-libs/validator-client", features = [ + "nymd-client", +] } +vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" } +# Used for Type conversion, can be extracted but its a lot of work +vesting-contract = { path = "../../contracts/vesting" } + +nym-types = { path = "../../common/types" } + +[features] +default = [] +generate-ts = [] + + + diff --git a/nym-wallet/nym-wallet-types/src/admin.rs b/nym-wallet/nym-wallet-types/src/admin.rs new file mode 100644 index 0000000000..0afaa6fb13 --- /dev/null +++ b/nym-wallet/nym-wallet-types/src/admin.rs @@ -0,0 +1,44 @@ +use std::convert::TryFrom; + +use cosmwasm_std::Uint128; +use serde::{Deserialize, Serialize}; + +use mixnet_contract_common::ContractStateParams; +use nym_types::error::TypesError; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "nym-wallet/src/types/rust/StateParams.ts") +)] +#[derive(Serialize, Deserialize, Debug)] +pub struct TauriContractStateParams { + minimum_mixnode_pledge: String, + minimum_gateway_pledge: String, + mixnode_rewarded_set_size: u32, + mixnode_active_set_size: u32, +} + +impl From for TauriContractStateParams { + fn from(p: ContractStateParams) -> TauriContractStateParams { + TauriContractStateParams { + minimum_mixnode_pledge: p.minimum_mixnode_pledge.to_string(), + minimum_gateway_pledge: p.minimum_gateway_pledge.to_string(), + mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, + mixnode_active_set_size: p.mixnode_active_set_size, + } + } +} + +impl TryFrom for ContractStateParams { + type Error = TypesError; + + fn try_from(p: TauriContractStateParams) -> Result { + Ok(ContractStateParams { + minimum_mixnode_pledge: Uint128::try_from(p.minimum_mixnode_pledge.as_str())?, + minimum_gateway_pledge: Uint128::try_from(p.minimum_gateway_pledge.as_str())?, + mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, + mixnode_active_set_size: p.mixnode_active_set_size, + }) + } +} diff --git a/nym-wallet/nym-wallet-types/src/app.rs b/nym-wallet/nym-wallet-types/src/app.rs new file mode 100644 index 0000000000..e381359ef4 --- /dev/null +++ b/nym-wallet/nym-wallet-types/src/app.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +#[allow(non_snake_case)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "nym-wallet/src/types/rust/AppEnv.ts") +)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +pub struct AppEnv { + pub ADMIN_ADDRESS: Option, + pub SHOW_TERMINAL: Option, +} diff --git a/nym-wallet/nym-wallet-types/src/epoch.rs b/nym-wallet/nym-wallet-types/src/epoch.rs new file mode 100644 index 0000000000..af11611ff2 --- /dev/null +++ b/nym-wallet/nym-wallet-types/src/epoch.rs @@ -0,0 +1,26 @@ +use mixnet_contract_common::Interval; +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "nym-wallet/src/types/rust/Epoch.ts") +)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize)] +pub struct Epoch { + id: u32, + start: i64, + end: i64, + duration_seconds: u64, +} + +impl From for Epoch { + fn from(interval: Interval) -> Self { + Self { + id: interval.id(), + start: interval.start_unix_timestamp(), + end: interval.end_unix_timestamp(), + duration_seconds: interval.length().as_secs(), + } + } +} diff --git a/nym-wallet/nym-wallet-types/src/lib.rs b/nym-wallet/nym-wallet-types/src/lib.rs new file mode 100644 index 0000000000..d6e6600a65 --- /dev/null +++ b/nym-wallet/nym-wallet-types/src/lib.rs @@ -0,0 +1,5 @@ +pub mod admin; +pub mod app; +pub mod epoch; +pub mod network; +pub mod network_config; diff --git a/nym-wallet/src-tauri/src/network.rs b/nym-wallet/nym-wallet-types/src/network.rs similarity index 76% rename from nym-wallet/src-tauri/src/network.rs rename to nym-wallet/nym-wallet-types/src/network.rs index 9f0e21f990..c653b3468b 100644 --- a/nym-wallet/src-tauri/src/network.rs +++ b/nym-wallet/nym-wallet-types/src/network.rs @@ -1,15 +1,21 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use config::defaults::all::Network as ConfigNetwork; -use config::defaults::{mainnet, qa, sandbox}; +use cosmrs::Denom; use serde::{Deserialize, Serialize}; use std::fmt; +use std::str::FromStr; use strum::EnumIter; +use config::defaults::all::Network as ConfigNetwork; +use config::defaults::{mainnet, qa, sandbox}; + #[allow(clippy::upper_case_acronyms)] -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/network.ts"))] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "nym-wallet/src/types/rust/Network.ts") +)] #[derive(Copy, Clone, Debug, Deserialize, EnumIter, Eq, Hash, PartialEq, Serialize)] pub enum Network { QA, @@ -22,12 +28,12 @@ impl Network { self.to_string().to_lowercase() } - pub fn denom(&self) -> &str { + pub fn denom(&self) -> Denom { match self { // network defaults should be correctly formatted - Network::QA => qa::DENOM, - Network::SANDBOX => sandbox::DENOM, - Network::MAINNET => mainnet::DENOM, + Network::QA => Denom::from_str(qa::DENOM).unwrap(), + Network::SANDBOX => Denom::from_str(sandbox::DENOM).unwrap(), + Network::MAINNET => Denom::from_str(mainnet::DENOM).unwrap(), } } } diff --git a/nym-wallet/nym-wallet-types/src/network_config.rs b/nym-wallet/nym-wallet-types/src/network_config.rs new file mode 100644 index 0000000000..b1f987d5b0 --- /dev/null +++ b/nym-wallet/nym-wallet-types/src/network_config.rs @@ -0,0 +1,50 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; + +// When the UI queries validator urls we use this type +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "nym-wallet/src/types/rust/ValidatorUrls.ts") +)] +#[derive(Debug, Serialize, Deserialize)] +pub struct ValidatorUrls { + pub urls: Vec, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "nym-wallet/src/types/rust/ValidatorUrl.ts") +)] +#[derive(Debug, Serialize, Deserialize)] +pub struct ValidatorUrl { + pub url: String, + pub name: Option, +} + +// The type used when adding or removing validators, effectively the input. +// NOTE: we should consider if we want to split this up +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "nym-wallet/src/types/rust/ValidatorUrls.ts") +)] +#[derive(Debug, Serialize, Deserialize)] +pub struct Validator { + pub nymd_url: String, + pub nymd_name: Option, + pub api_url: Option, +} + +impl fmt::Display for Validator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let nymd_url = format!("nymd_url: {}", self.nymd_url); + let api_url = self + .api_url + .as_ref() + .map(|api_url| format!(", api_url: {}", api_url)) + .unwrap_or_default(); + write!(f, "{nymd_url}{api_url}") + } +} diff --git a/nym-wallet/package.json b/nym-wallet/package.json index b6044f64b0..30c115f8e5 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -28,8 +28,11 @@ "@mui/icons-material": "^5.2.0", "@mui/material": "^5.2.2", "@mui/styles": "^5.2.2", + "@mui/utils": "^5.7.0", "@nymproject/mui-theme": "^1.0.0", "@nymproject/react": "^1.0.0", + "@nymproject/types": "^1.0.0", + "@storybook/react": "^6.4.22", "@tauri-apps/api": "^1.0.0-rc.1", "bs58": "^4.0.1", "clsx": "^1.1.1", @@ -41,7 +44,7 @@ "react-dom": "^17.0.2", "react-error-boundary": "^3.1.3", "react-hook-form": "^7.14.2", - "react-router-dom": "^5.2.0", + "react-router-dom": "6", "semver": "^6.3.0", "string-to-color": "^2.2.2", "use-clipboard-copy": "^0.2.0", @@ -55,12 +58,6 @@ "@babel/preset-react": "^7.14.5", "@nymproject/eslint-config-react-typescript": "^1.0.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", - "@storybook/addon-actions": "^6.4.19", - "@storybook/addon-essentials": "^6.4.19", - "@storybook/addon-interactions": "^6.4.19", - "@storybook/addon-links": "^6.4.19", - "@storybook/react": "^6.4.19", - "@storybook/testing-library": "^0.0.9", "@svgr/webpack": "^6.1.1", "@tauri-apps/cli": "^1.0.0-rc.5", "@testing-library/jest-dom": "^5.14.1", diff --git a/nym-wallet/public/index.html b/nym-wallet/public/index.html index e9696fdc50..af101025cb 100644 --- a/nym-wallet/public/index.html +++ b/nym-wallet/public/index.html @@ -1,11 +1,11 @@ - - - - Nym Wallet - - -
- + + + + Nym Wallet + + +
+ diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 2c28fcdc1e..251722ac91 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -38,7 +38,7 @@ strum = { version = "0.23", features = ["derive"] } tauri = { version = "=1.0.0-rc.2", features = ["clipboard-all", "shell-open", "updater", "window-maximize"] } tendermint-rpc = "0.23.0" thiserror = "1.0" -tokio = { version = "1.19.1", features = ["sync", "time"] } +tokio = { version = "1.10", features = ["sync", "time"] } toml = "0.5.8" url = "2.2" @@ -48,6 +48,7 @@ base64 = "0.13" zeroize = "1.4.3" cosmwasm-std = "1.0.0" +cosmrs = "0.7.0" validator-client = { path = "../../common/client-libs/validator-client", features = [ "nymd-client", @@ -58,11 +59,14 @@ config = { path = "../../common/config" } coconut-interface = { path = "../../common/coconut-interface" } # Used for Type conversion, can be extracted but its a lot of work vesting-contract = { path = "../../contracts/vesting" } +nym-types = { path = "../../common/types" } +nym-wallet-types = { path = "../nym-wallet-types" } [dev-dependencies] -ts-rs = "6.1.2" tempfile = "3.3.0" +ts-rs = "6.1.2" [features] default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] +generate-ts = [] diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index 3184cab8c9..6ac65f3b45 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -1,22 +1,26 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::network_config; -use crate::platform_constants::{CONFIG_DIR_NAME, CONFIG_FILENAME}; -use crate::{error::BackendError, network::Network as WalletNetwork}; -use config::defaults::all::Network; -use config::defaults::{all::SupportedNetworks, ValidatorDetails}; use core::fmt; -use itertools::Itertools; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::str::FromStr; use std::{fs, io, path::PathBuf}; + +use itertools::Itertools; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use url::Url; use validator_client::nymd::AccountId as CosmosAccountId; +use config::defaults::all::Network; +use config::defaults::{all::SupportedNetworks, ValidatorDetails}; +use nym_wallet_types::network::Network as WalletNetwork; +use nym_wallet_types::network_config; + +use crate::error::BackendError; +use crate::platform_constants::{CONFIG_DIR_NAME, CONFIG_FILENAME}; + pub const REMOTE_SOURCE_OF_VALIDATOR_URLS: &str = "https://nymtech.net/.wellknown/wallet/validators.json"; diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index e0fb1e565b..04c954e6cb 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -1,3 +1,4 @@ +use nym_types::error::TypesError; use serde::{Serialize, Serializer}; use std::io; use std::num::ParseIntError; @@ -7,6 +8,11 @@ use validator_client::{nymd::error::NymdError, ValidatorClientError}; #[derive(Error, Debug)] pub enum BackendError { + #[error("{source}")] + TypesError { + #[from] + source: TypesError, + }, #[error("{source}")] Bip39Error { #[from] @@ -70,16 +76,10 @@ pub enum BackendError { ClientNotInitialized, #[error("No balance available for address {0}")] NoBalance(String), - #[error("{0} is not a valid denomination string")] - InvalidDenom(String), - #[error("{0} is not a valid network denomination string")] - InvalidNetworkDenom(String), #[error("The provided network is not supported (yet)")] NetworkNotSupported(config::defaults::all::Network), #[error("Could not access the local data storage directory")] UnknownStorageDirectory, - #[error("No validator API URL configured")] - NoValidatorApiUrlConfigured, #[error("The wallet file already exists")] WalletFileAlreadyExists, #[error("The wallet file is not found")] @@ -96,10 +96,10 @@ pub enum BackendError { WalletMnemonicAlreadyExistsInWalletLogin, #[error("Adding a different password to the wallet not currently supported")] WalletDifferentPasswordDetected, - #[error("Unexpted mnemonic account for login")] + #[error("Unexpected mnemonic account for login")] WalletUnexpectedMnemonicAccount, - #[error("Unexpted multiple account entry for login")] - WalletUnexpectedMultipleAccounts, + // #[error("Unexpected multiple account entry for login")] + // WalletUnexpectedMultipleAccounts, #[error("Failed to derive address from mnemonic")] FailedToDeriveAddress, #[error("{0}")] diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index ddb43f08fc..a8efcf1bf8 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -3,19 +3,14 @@ windows_subsystem = "windows" )] -use crate::menu::AddDefaultSubmenus; -use crate::operations::{mixnet, simulate, validator_api, vesting}; -use crate::state::State; use mixnet_contract_common::{Gateway, MixNode}; use std::sync::Arc; use tauri::Menu; use tokio::sync::RwLock; -mod coin; mod config; mod error; mod menu; -mod network; mod network_config; mod operations; mod platform_constants; @@ -23,6 +18,14 @@ mod state; mod utils; mod wallet_storage; +use crate::menu::AddDefaultSubmenus; +use crate::operations::mixnet; +use crate::operations::simulate; +use crate::operations::validator_api; +use crate::operations::vesting; + +use crate::state::State; + fn main() { dotenv::dotenv().ok(); setup_logging(); @@ -32,7 +35,6 @@ fn main() { .invoke_handler(tauri::generate_handler![ mixnet::account::add_account_for_password, mixnet::account::connect_with_mnemonic, - mixnet::account::create_new_account, mixnet::account::create_new_mnemonic, mixnet::account::create_password, mixnet::account::does_password_file_exist, @@ -59,7 +61,9 @@ fn main() { mixnet::delegate::delegate_to_mixnode, mixnet::delegate::get_delegator_rewards, mixnet::delegate::get_pending_delegation_events, - mixnet::delegate::get_reverse_mix_delegations_paged, + mixnet::delegate::get_delegation_summary, + mixnet::delegate::get_all_pending_delegation_events, + mixnet::delegate::get_all_mix_delegations, mixnet::delegate::undelegate_from_mixnode, mixnet::epoch::get_current_epoch, mixnet::rewards::claim_delegator_reward, @@ -76,8 +80,6 @@ fn main() { network_config::update_validator_urls, state::load_config_from_files, state::save_config_to_files, - utils::major_to_minor, - utils::minor_to_major, utils::owns_gateway, utils::owns_mixnode, utils::get_env, @@ -114,19 +116,18 @@ fn main() { vesting::queries::vesting_get_gateway_pledge, vesting::queries::vesting_get_mixnode_pledge, vesting::queries::vesting_start_time, - // simulate endpoints simulate::admin::simulate_update_contract_settings, + simulate::cosmos::simulate_send, simulate::mixnet::simulate_bond_gateway, - simulate::mixnet::simulate_bond_mixnode, simulate::mixnet::simulate_unbond_gateway, + simulate::mixnet::simulate_bond_mixnode, simulate::mixnet::simulate_unbond_mixnode, simulate::mixnet::simulate_update_mixnode, simulate::mixnet::simulate_delegate_to_mixnode, simulate::mixnet::simulate_undelegate_from_mixnode, - simulate::cosmos::simulate_send, simulate::vesting::simulate_vesting_bond_gateway, - simulate::vesting::simulate_vesting_bond_mixnode, simulate::vesting::simulate_vesting_unbond_gateway, + simulate::vesting::simulate_vesting_bond_mixnode, simulate::vesting::simulate_vesting_unbond_mixnode, simulate::vesting::simulate_vesting_update_mixnode, simulate::vesting::simulate_withdraw_vested_coins, @@ -153,6 +154,10 @@ fn setup_logging() { log_builder.filter(None, log::LevelFilter::Info); } + if ::std::env::var("RUST_TRACE_OPERATIONS").is_ok() { + log_builder.filter_module("nym_wallet::operations", log::LevelFilter::Trace); + } + log_builder .filter_module("hyper", log::LevelFilter::Warn) .filter_module("tokio_reactor", log::LevelFilter::Warn) diff --git a/nym-wallet/src-tauri/src/network_config.rs b/nym-wallet/src-tauri/src/network_config.rs index 75a8e38eec..2a621922d5 100644 --- a/nym-wallet/src-tauri/src/network_config.rs +++ b/nym-wallet/src-tauri/src/network_config.rs @@ -1,52 +1,15 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::BackendError; -use crate::network::Network as WalletNetwork; -use crate::state::State; +use std::sync::Arc; -use serde::{Deserialize, Serialize}; -use std::{fmt, sync::Arc}; use tokio::sync::RwLock; -// When the UI queries validator urls we use this type -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurls.ts"))] -#[derive(Debug, Serialize, Deserialize)] -pub struct ValidatorUrls { - pub urls: Vec, -} +use nym_wallet_types::network::Network as WalletNetwork; +use nym_wallet_types::network_config::{Validator, ValidatorUrl, ValidatorUrls}; -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurl.ts"))] -#[derive(Debug, Serialize, Deserialize)] -pub struct ValidatorUrl { - pub url: String, - pub name: Option, -} - -// The type used when adding or removing validators, effectively the input. -// NOTE: we should consider if we want to split this up -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurls.ts"))] -#[derive(Debug, Serialize, Deserialize)] -pub struct Validator { - pub nymd_url: String, - pub nymd_name: Option, - pub api_url: Option, -} - -impl fmt::Display for Validator { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let nymd_url = format!("nymd_url: {}", self.nymd_url); - let api_url = self - .api_url - .as_ref() - .map(|api_url| format!(", api_url: {}", api_url)) - .unwrap_or_default(); - write!(f, "{nymd_url}{api_url}") - } -} +use crate::error::BackendError; +use crate::state::State; #[tauri::command] pub async fn get_validator_nymd_urls( diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index 4d535a01f1..ae2635a18b 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -1,71 +1,27 @@ -use crate::coin::{Coin, Denom}; use crate::config::{Config, CUSTOM_SIMULATED_GAS_MULTIPLIER}; use crate::error::BackendError; -use crate::network::Network as WalletNetwork; use crate::network_config; use crate::nymd_client; use crate::state::{State, WalletAccountIds}; use crate::wallet_storage::{self, DEFAULT_LOGIN_ID}; - use bip39::{Language, Mnemonic}; use config::defaults::all::Network; use config::defaults::COSMOS_DERIVATION_PATH; +use cosmrs::bip32::DerivationPath; use itertools::Itertools; +use nym_types::account::{Account, AccountEntry, Balance}; +use nym_types::currency::MajorCurrencyAmount; +use nym_wallet_types::network::Network as WalletNetwork; use rand::seq::SliceRandom; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::convert::TryInto; use std::str::FromStr; use std::sync::Arc; use strum::IntoEnumIterator; use tokio::sync::RwLock; use url::Url; -use validator_client::nymd::bip32::DerivationPath; use validator_client::nymd::wallet::{AccountData, DirectSecp256k1HdWallet}; -use validator_client::nymd::{AccountId as CosmosAccountId, SigningNymdClient}; -use validator_client::Client; - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/account.ts"))] -#[derive(Serialize, Deserialize)] -pub struct Account { - contract_address: String, - client_address: String, - denom: Denom, -} - -impl Account { - pub fn new(contract_address: String, client_address: String, denom: Denom) -> Self { - Account { - contract_address, - client_address, - denom, - } - } -} - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/createdaccount.ts"))] -#[derive(Serialize, Deserialize)] -pub struct CreatedAccount { - account: Account, - mnemonic: String, -} - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/createdaccount.ts"))] -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct AccountEntry { - id: String, - address: String, -} - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/balance.ts"))] -#[derive(Serialize, Deserialize)] -pub struct Balance { - coin: Coin, - printable_balance: String, -} +use validator_client::{nymd::SigningNymdClient, Client}; #[tauri::command] pub async fn connect_with_mnemonic( @@ -80,17 +36,17 @@ pub async fn connect_with_mnemonic( pub async fn get_balance( state: tauri::State<'_, Arc>>, ) -> Result { - let denom = state.read().await.current_network().denom().to_owned(); + let denom = state.read().await.current_network().denom(); match nymd_client!(state) - .get_balance(nymd_client!(state).address(), denom.parse()?) + .get_balance(nymd_client!(state).address(), denom) .await { Ok(Some(coin)) => { - let coin = Coin::new(&coin.amount.to_string(), &Denom::from_str(&coin.denom)?); + let amount = MajorCurrencyAmount::from(coin); + let printable_balance = amount.to_string(); Ok(Balance { - coin: coin.clone(), - // haha, that's so junky : ) - printable_balance: format!("{} {}", coin.to_major().amount(), &denom[1..]), + amount, + printable_balance, }) } Ok(None) => Err(BackendError::NoBalance( @@ -100,18 +56,6 @@ pub async fn get_balance( } } -#[tauri::command] -pub async fn create_new_account( - state: tauri::State<'_, Arc>>, -) -> Result { - let rand_mnemonic = random_mnemonic(); - let account = connect_with_mnemonic(rand_mnemonic.to_string(), state).await?; - Ok(CreatedAccount { - account, - mnemonic: rand_mnemonic.to_string(), - }) -} - #[tauri::command] pub fn create_new_mnemonic() -> String { random_mnemonic().to_string() @@ -135,7 +79,7 @@ pub async fn switch_network( Account::new( client.nymd.mixnet_contract_address().to_string(), client.nymd.address().to_string(), - denom.parse()?, + denom.try_into()?, ) }; @@ -218,7 +162,7 @@ async fn _connect_with_mnemonic( Some(client) => Ok(Account::new( client.nymd.mixnet_contract_address().to_string(), client.nymd.address().to_string(), - default_network.denom().parse()?, + default_network.denom().try_into()?, )), None => Err(BackendError::NetworkNotSupported( config::defaults::DEFAULT_NETWORK, @@ -467,7 +411,7 @@ pub async fn add_account_for_password( account_id: &str, state: tauri::State<'_, Arc>>, ) -> Result { - log::info!("Adding account: {account_id}"); + log::info!("Adding account for the current password: {account_id}"); let mnemonic = Mnemonic::from_str(mnemonic)?; let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); // Currently we only support a single, default, login id in the wallet @@ -523,7 +467,7 @@ async fn set_state_with_all_accounts( .iter() .map(|account| { let mnemonic = account.mnemonic(); - let addresses: HashMap = WalletNetwork::iter() + let addresses: HashMap = WalletNetwork::iter() .map(|network| { let config_network: Network = network.into(); ( @@ -568,7 +512,7 @@ pub async fn remove_account_for_password( fn derive_address( mnemonic: bip39::Mnemonic, prefix: &str, -) -> Result { +) -> Result { DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic)? .try_derive_accounts()? .first() @@ -637,8 +581,6 @@ fn _show_mnemonic_for_account_in_password( #[cfg(test)] mod tests { - use super::*; - use std::path::PathBuf; use crate::wallet_storage::{ @@ -646,6 +588,8 @@ mod tests { account_data::{MnemonicAccount, WalletAccount}, }; + use super::*; + // This decryptes a stored wallet file using the same procedure as when signing in. Most tests // related to the encryped wallet storage is in `wallet_storage`. #[test] diff --git a/nym-wallet/src-tauri/src/operations/mixnet/admin.rs b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs index 598e108d97..abfa3a48b5 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/admin.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs @@ -1,53 +1,24 @@ +use std::convert::TryInto; +use std::sync::Arc; + +use tokio::sync::RwLock; + +use mixnet_contract_common::ContractStateParams; +use nym_wallet_types::admin::TauriContractStateParams; +use validator_client::nymd::Fee; + use crate::error::BackendError; use crate::nymd_client; use crate::state::State; -use cosmwasm_std::Uint128; -use mixnet_contract_common::ContractStateParams; -use serde::{Deserialize, Serialize}; -use std::convert::{TryFrom, TryInto}; -use std::sync::Arc; -use tokio::sync::RwLock; -use validator_client::nymd::Fee; - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/stateparams.ts"))] -#[derive(Serialize, Deserialize)] -pub struct TauriContractStateParams { - minimum_mixnode_pledge: String, - minimum_gateway_pledge: String, - mixnode_rewarded_set_size: u32, - mixnode_active_set_size: u32, -} - -impl From for TauriContractStateParams { - fn from(p: ContractStateParams) -> TauriContractStateParams { - TauriContractStateParams { - minimum_mixnode_pledge: p.minimum_mixnode_pledge.to_string(), - minimum_gateway_pledge: p.minimum_gateway_pledge.to_string(), - mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, - mixnode_active_set_size: p.mixnode_active_set_size, - } - } -} - -impl TryFrom for ContractStateParams { - type Error = BackendError; - - fn try_from(p: TauriContractStateParams) -> Result { - Ok(ContractStateParams { - minimum_mixnode_pledge: Uint128::try_from(p.minimum_mixnode_pledge.as_str())?, - minimum_gateway_pledge: Uint128::try_from(p.minimum_gateway_pledge.as_str())?, - mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, - mixnode_active_set_size: p.mixnode_active_set_size, - }) - } -} #[tauri::command] pub async fn get_contract_settings( state: tauri::State<'_, Arc>>, ) -> Result { - Ok(nymd_client!(state).get_contract_settings().await?.into()) + log::info!(">>> Getting contract settings"); + let res = nymd_client!(state).get_contract_settings().await?.into(); + log::trace!("<<< {:?}", res); + Ok(res) } #[tauri::command] @@ -57,8 +28,14 @@ pub async fn update_contract_settings( state: tauri::State<'_, Arc>>, ) -> Result { let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; + log::info!( + ">>> Updating contract settings: {:?}", + mixnet_contract_settings_params + ); nymd_client!(state) .update_contract_settings(mixnet_contract_settings_params.clone(), fee) .await?; - Ok(mixnet_contract_settings_params.into()) + let res = mixnet_contract_settings_params.into(); + log::trace!("<<< {:?}", res); + Ok(res) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index e940ff60ac..7c20d1bab6 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -1,60 +1,101 @@ -use crate::coin::Coin; use crate::error::BackendError; use crate::nymd_client; use crate::state::State; use crate::{Gateway, MixNode}; -use cosmwasm_std::Uint128; -use mixnet_contract_common::{GatewayBond, MixNodeBond}; +use nym_types::currency::MajorCurrencyAmount; +use nym_types::gateway::GatewayBond; +use nym_types::mixnode::MixNodeBond; +use nym_types::transaction::TransactionExecuteResult; use std::sync::Arc; use tokio::sync::RwLock; -use validator_client::nymd::Fee; +use validator_client::nymd::{CosmWasmCoin, Fee}; #[tauri::command] pub async fn bond_gateway( gateway: Gateway, - pledge: Coin, + pledge: MajorCurrencyAmount, owner_signature: String, fee: Option, state: tauri::State<'_, Arc>>, -) -> Result<(), BackendError> { - let pledge = pledge.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .bond_gateway(gateway, owner_signature, pledge, fee) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + let pledge_minor = pledge.clone().into(); + log::info!( + ">>> Bond gateway: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", + &gateway.identity_key, + pledge, + &pledge_minor, + fee, + ); + let res = nymd_client!(state) + .bond_gateway(gateway, owner_signature, pledge_minor, fee) .await?; - Ok(()) + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn unbond_gateway( fee: Option, state: tauri::State<'_, Arc>>, -) -> Result<(), BackendError> { - nymd_client!(state).unbond_gateway(fee).await?; - Ok(()) -} - -#[tauri::command] -pub async fn unbond_mixnode( - fee: Option, - state: tauri::State<'_, Arc>>, -) -> Result<(), BackendError> { - nymd_client!(state).unbond_mixnode(fee).await?; - Ok(()) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + log::info!(">>> Unbond gateway, fee = {:?}", fee); + let res = nymd_client!(state).unbond_gateway(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn bond_mixnode( mixnode: MixNode, owner_signature: String, - pledge: Coin, + pledge: MajorCurrencyAmount, fee: Option, state: tauri::State<'_, Arc>>, -) -> Result<(), BackendError> { - let pledge = pledge.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .bond_mixnode(mixnode, owner_signature, pledge, fee) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + let pledge_minor = pledge.clone().into(); + log::info!( + ">>> Bond mixnode: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", + mixnode.identity_key, + pledge, + pledge_minor, + fee, + ); + let res = nymd_client!(state) + .bond_mixnode(mixnode, owner_signature, pledge_minor, fee) .await?; - Ok(()) + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) +} + +#[tauri::command] +pub async fn unbond_mixnode( + fee: Option, + state: tauri::State<'_, Arc>>, +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + log::info!(">>> Unbond mixnode, fee = {:?}", fee); + let res = nymd_client!(state).unbond_mixnode(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] @@ -62,37 +103,72 @@ pub async fn update_mixnode( profit_margin_percent: u8, fee: Option, state: tauri::State<'_, Arc>>, -) -> Result<(), BackendError> { - nymd_client!(state) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Update mixnode: profit_margin_percent = {}, fee {:?}", + profit_margin_percent, + fee, + ); + let res = nymd_client!(state) .update_mixnode_config(profit_margin_percent, fee) .await?; - Ok(()) + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn mixnode_bond_details( state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { + log::info!(">>> Get mixnode bond details"); let guard = state.read().await; let client = guard.current_client()?; let bond = client.nymd.owns_mixnode(client.nymd.address()).await?; - Ok(bond) + let res = MixNodeBond::from_mixnet_contract_mixnode_bond(bond)?; + log::info!( + "<<< identity_key = {:?}", + res.as_ref().map(|r| r.mix_node.identity_key.to_string()) + ); + log::trace!("<<< {:?}", res); + Ok(res) } #[tauri::command] pub async fn gateway_bond_details( state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { + log::info!(">>> Get gateway bond details"); let guard = state.read().await; let client = guard.current_client()?; let bond = client.nymd.owns_gateway(client.nymd.address()).await?; - Ok(bond) + let res = GatewayBond::from_mixnet_contract_gateway_bond(bond)?; + log::info!( + "<<< identity_key = {:?}", + res.as_ref().map(|r| r.gateway.identity_key.to_string()) + ); + log::trace!("<<< {:?}", res); + Ok(res) } #[tauri::command] pub async fn get_operator_rewards( address: String, state: tauri::State<'_, Arc>>, -) -> Result { - Ok(nymd_client!(state).get_operator_rewards(address).await?) +) -> Result { + log::info!(">>> Get operator rewards for {}", address); + let denom = state.read().await.current_network().denom(); + let rewards_as_minor = nymd_client!(state).get_operator_rewards(address).await?; + let coin = CosmWasmCoin::new(rewards_as_minor.u128(), denom.as_ref()); + let amount: MajorCurrencyAmount = coin.into(); + log::info!( + "<<< rewards_as_minor = {}, amount = {}", + rewards_as_minor, + amount + ); + Ok(amount) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index c559b18e43..438256d8ff 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -1,11 +1,16 @@ -use crate::coin::Coin; use crate::error::BackendError; -use crate::nymd_client; use crate::state::State; -use crate::utils::DelegationEvent; -use crate::utils::DelegationResult; -use cosmwasm_std::Uint128; -use mixnet_contract_common::{IdentityKey, PagedDelegatorDelegationsResponse}; +use crate::vesting::delegate::get_pending_vesting_delegation_events; +use crate::{api_client, nymd_client}; +use cosmwasm_std::Coin as CosmWasmCoin; +use mixnet_contract_common::IdentityKey; +use nym_types::currency::{CurrencyDenom, MajorCurrencyAmount}; +use nym_types::delegation::{ + from_contract_delegation_events, Delegation, DelegationEvent, DelegationRecord, + DelegationWithEverything, DelegationsSummaryResponse, +}; +use nym_types::transaction::TransactionExecuteResult; +use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use validator_client::nymd::Fee; @@ -14,30 +19,44 @@ use validator_client::nymd::Fee; pub async fn get_pending_delegation_events( state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - Ok(nymd_client!(state) + log::info!(">>> Get pending delegation events"); + let events = nymd_client!(state) .get_pending_delegation_events(nymd_client!(state).address().to_string(), None) - .await? - .into_iter() - .map(|delegation_event| delegation_event.into()) - .collect::>()) + .await?; + log::info!("<<< {} pending delegation events", events.len()); + log::trace!("<<< pending delegation events = {:?}", events); + + match from_contract_delegation_events(events) { + Ok(res) => Ok(res), + Err(e) => Err(e.into()), + } } #[tauri::command] pub async fn delegate_to_mixnode( identity: &str, - amount: Coin, + amount: MajorCurrencyAmount, fee: Option, state: tauri::State<'_, Arc>>, -) -> Result { - let delegation = amount.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .delegate_to_mixnode(identity, delegation.clone(), fee) - .await?; - Ok(DelegationResult::new( - nymd_client!(state).address().as_ref(), +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + let delegation = amount.clone().into(); + log::info!( + ">>> Delegate to mixnode: identity_key = {}, amount = {}, minor_amount = {}, fee = {:?}", identity, - Some(delegation.into()), - )) + amount, + delegation, + fee, + ); + let res = nymd_client!(state) + .delegate_to_mixnode(identity, delegation, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] @@ -45,24 +64,240 @@ pub async fn undelegate_from_mixnode( identity: &str, fee: Option, state: tauri::State<'_, Arc>>, -) -> Result { - nymd_client!(state) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Undelegate from mixnode: identity_key = {}, fee = {:?}", + identity, + fee + ); + let res = nymd_client!(state) .remove_mixnode_delegation(identity, fee) .await?; - Ok(DelegationResult::new( - nymd_client!(state).address().as_ref(), - identity, - None, - )) + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) +} + +struct DelegationWithHistory { + pub delegation: Delegation, + pub amount_sum: MajorCurrencyAmount, + pub history: Vec, } #[tauri::command] -pub async fn get_reverse_mix_delegations_paged( +pub async fn get_all_mix_delegations( state: tauri::State<'_, Arc>>, -) -> Result { - Ok(nymd_client!(state) - .get_delegator_delegations_paged(nymd_client!(state).address().to_string(), None, None) - .await?) +) -> Result, BackendError> { + log::info!(">>> Get all mixnode delegations"); + + // TODO: add endpoint to validator API to get a single mix node bond + let mixnodes = api_client!(state).get_mixnodes().await?; + + let address = nymd_client!(state).address().to_string(); + + let denom_minor = state.read().await.current_network().denom(); + let denom: CurrencyDenom = denom_minor.clone().try_into()?; + + log::info!(" >>> Get delegations"); + let delegations = nymd_client!(state) + .get_delegator_delegations_paged(address.clone(), None, None) // get all delegations, ignoring paging + .await? + .delegations; + log::info!(" <<< {} delegations", delegations.len()); + + // first get pending events from the mixnet contract (operations made with unlocked tokens) + let mut pending_events_for_account = get_pending_delegation_events(state.clone()).await?; + + // then get pending events from the vesting contract (operations made with locked tokens) + let pending_vesting_events = get_pending_vesting_delegation_events(state.clone()).await?; + for event in pending_vesting_events { + pending_events_for_account.push(event); + } + + log::info!( + " <<< {} pending delegation events for account", + pending_events_for_account.len() + ); + + let mut map: HashMap = HashMap::new(); + + for pending_event in pending_events_for_account.clone() { + if delegations + .iter() + .any(|d| d.node_identity == pending_event.node_identity) + { + let amount = pending_event + .amount + .unwrap_or_else(|| MajorCurrencyAmount::zero(&denom)); + let delegation = DelegationWithHistory { + delegation: Delegation { + amount: amount.clone(), + node_identity: pending_event.node_identity, + proxy: None, + owner: pending_event.address, + block_height: pending_event.block_height, + }, + amount_sum: amount, + history: vec![], + }; + map.insert(delegation.delegation.node_identity.clone(), delegation); + } + } + + for d in delegations { + // create record of delegation + let delegated_on_iso_datetime = nymd_client!(state) + .get_block_timestamp(Some(d.block_height as u32)) + .await? + .to_rfc3339(); + let amount: MajorCurrencyAmount = d.amount.clone().into(); + let record = DelegationRecord { + amount: amount.clone(), + block_height: d.block_height, + delegated_on_iso_datetime, + }; + + let entry = map + .entry(d.node_identity.clone()) + .or_insert(DelegationWithHistory { + delegation: d.try_into()?, + history: vec![], + amount_sum: MajorCurrencyAmount::zero(&amount.denom), + }); + + entry.history.push(record); + entry.amount_sum = entry.amount_sum.clone() + amount; + } + + let mut with_everything: Vec = vec![]; + + for item in map { + let d = item.1.delegation; + let history = item.1.history; + let Delegation { + owner, + node_identity, + amount, + block_height, + proxy, + } = d; + + log::trace!( + " --- Delegation: node_identity = {}, amount = {}", + node_identity, + amount + ); + + let mixnode = mixnodes + .iter() + .find(|m| m.mix_node.identity_key == node_identity); + + let pledge_amount: Option = + mixnode.and_then(|m| m.pledge_amount.clone().try_into().ok()); + + let total_delegation: Option = + mixnode.and_then(|m| m.total_delegation.clone().try_into().ok()); + + let profit_margin_percent: Option = mixnode.map(|m| m.mix_node.profit_margin_percent); + + log::trace!(" >>> Get accumulated rewards: address = {}", address); + let accumulated_rewards = match nymd_client!(state) + .get_delegator_rewards(address.clone(), node_identity.clone(), proxy.clone()) + .await + { + Ok(rewards) => { + let reward = CosmWasmCoin { + denom: denom_minor.to_string(), + amount: rewards, + }; + let amount = MajorCurrencyAmount::from(reward); + log::trace!(" <<< rewards = {}, amount = {}", rewards, amount); + Some(amount) + } + Err(_) => { + log::trace!(" <<< no rewards waiting"); + None + } + }; + + let pending_events = + filter_pending_events(&node_identity, pending_events_for_account.clone()); + log::trace!( + " --- pending events for mixnode = {}", + pending_events.len() + ); + + log::trace!( + " >>> Get stake saturation: node_identity = {}", + node_identity + ); + let stake_saturation = api_client!(state) + .get_mixnode_stake_saturation(&node_identity) + .await + .ok() + .map(|r| r.saturation); + log::trace!(" <<< {:?}", stake_saturation); + + log::trace!( + " >>> Get average uptime percentage: node_identity = {}", + node_identity + ); + let avg_uptime_percent = api_client!(state) + .get_mixnode_avg_uptime(&node_identity) + .await + .ok() + .map(|r| r.avg_uptime); + log::trace!(" <<< {:?}", avg_uptime_percent); + + log::trace!( + " >>> Convert delegated on block height to timestamp: block_height = {}", + d.block_height + ); + let timestamp = nymd_client!(state) + .get_block_timestamp(Some(d.block_height as u32)) + .await?; + let delegated_on_iso_datetime = timestamp.to_rfc3339(); + log::trace!( + " <<< timestamp = {:?}, delegated_on_iso_datetime = {:?}", + timestamp, + delegated_on_iso_datetime + ); + + with_everything.push(DelegationWithEverything { + owner: owner.to_string(), + node_identity: node_identity.to_string(), + amount: item.1.amount_sum, + block_height, + proxy: proxy.clone(), + delegated_on_iso_datetime, + stake_saturation, + accumulated_rewards, + profit_margin_percent, + pledge_amount, + avg_uptime_percent, + total_delegation, + pending_events, + history, + }) + } + log::trace!("<<< {:?}", with_everything); + + Ok(with_everything) +} + +fn filter_pending_events( + node_identity: &str, + pending_events: Vec, +) -> Vec { + pending_events + .iter() + .filter(|e| e.node_identity == node_identity) + .cloned() + .collect::>() } #[tauri::command] @@ -71,8 +306,70 @@ pub async fn get_delegator_rewards( mix_identity: IdentityKey, proxy: Option, state: tauri::State<'_, Arc>>, -) -> Result { - Ok(nymd_client!(state) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Get delegator rewards: mix_identity = {}, proxy = {:?}", + mix_identity, + proxy + ); + let res = nymd_client!(state) .get_delegator_rewards(address, mix_identity, proxy) - .await?) + .await?; + let coin = CosmWasmCoin::new(res.u128(), denom_minor.as_ref()); + let amount = coin.into(); + log::info!(">>> res = {}, amount = {}", res, amount); + Ok(amount) +} + +#[tauri::command] +pub async fn get_delegation_summary( + state: tauri::State<'_, Arc>>, +) -> Result { + log::info!(">>> Get delegation summary"); + + let denom_minor = state.read().await.current_network().denom(); + let denom: CurrencyDenom = denom_minor.clone().try_into()?; + + let delegations = get_all_mix_delegations(state.clone()).await?; + let mut total_delegations = MajorCurrencyAmount::zero(&denom); + let mut total_rewards = MajorCurrencyAmount::zero(&denom); + + for d in delegations.clone() { + total_delegations = total_delegations + d.amount; + if let Some(rewards) = d.accumulated_rewards { + total_rewards = total_rewards + rewards; + } + } + log::info!( + "<<< {} delegations, total_delegations = {}, total_rewards = {}", + delegations.len(), + total_delegations, + total_rewards + ); + log::trace!("<<< {:?}", delegations); + + Ok(DelegationsSummaryResponse { + delegations, + total_delegations, + total_rewards, + }) +} + +#[tauri::command] +pub async fn get_all_pending_delegation_events( + state: tauri::State<'_, Arc>>, +) -> Result, BackendError> { + log::info!(">>> Get all pending delegation events"); + + // get pending events from mixnet and vesting contract + let mut pending_events_for_account = get_pending_delegation_events(state.clone()).await?; + let pending_vesting_events = get_pending_vesting_delegation_events(state.clone()).await?; + + // combine them + for event in pending_vesting_events { + pending_events_for_account.push(event); + } + + Ok(pending_events_for_account) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs b/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs index 263e0fcf6d..5203e5bc62 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs @@ -1,36 +1,16 @@ use crate::error::BackendError; use crate::nymd_client; use crate::state::State; -use mixnet_contract_common::Interval; -use serde::{Deserialize, Serialize}; +use nym_wallet_types::epoch::Epoch; use std::sync::Arc; use tokio::sync::RwLock; -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/epoch.ts"))] -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize)] -pub struct Epoch { - id: u32, - start: i64, - end: i64, - duration_seconds: u64, -} - -impl From for Epoch { - fn from(interval: Interval) -> Self { - Self { - id: interval.id(), - start: interval.start_unix_timestamp(), - end: interval.end_unix_timestamp(), - duration_seconds: interval.length().as_secs(), - } - } -} - #[tauri::command] pub async fn get_current_epoch( state: tauri::State<'_, Arc>>, ) -> Result { + log::info!(">>> Get curren epoch"); let interval = nymd_client!(state).get_current_epoch().await?; + log::info!("<<< curren epoch = {}", interval); Ok(interval.into()) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/send.rs b/nym-wallet/src-tauri/src/operations/mixnet/send.rs index 52a7258dfc..30efde8566 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/send.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/send.rs @@ -1,69 +1,46 @@ -use crate::coin::Coin; use crate::error::BackendError; use crate::nymd_client; use crate::state::State; -use serde::{Deserialize, Serialize}; +use nym_types::currency::MajorCurrencyAmount; +use nym_types::transaction::{SendTxResult, TransactionDetails}; use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; -use validator_client::nymd::{AccountId, Fee, TxResponse}; - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/tauritxresult.ts"))] -#[derive(Deserialize, Serialize)] -pub struct TauriTxResult { - block_height: u64, - code: u32, - details: TransactionDetails, - gas_used: u64, - gas_wanted: u64, - tx_hash: String, -} - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr( - test, - ts(export, export_to = "../src/types/rust/transactiondetails.ts") -)] -#[derive(Deserialize, Serialize)] -pub struct TransactionDetails { - amount: Coin, - from_address: String, - to_address: String, -} - -impl TauriTxResult { - fn new(t: TxResponse, details: TransactionDetails) -> TauriTxResult { - TauriTxResult { - block_height: t.height.value(), - code: t.tx_result.code.value(), - details, - gas_used: t.tx_result.gas_used.value(), - gas_wanted: t.tx_result.gas_wanted.value(), - tx_hash: t.hash.to_string(), - } - } -} +use validator_client::nymd::{AccountId, Fee}; #[tauri::command] pub async fn send( address: &str, - amount: Coin, + amount: MajorCurrencyAmount, memo: String, fee: Option, state: tauri::State<'_, Arc>>, -) -> Result { +) -> Result { + let denom_minor = state.read().await.current_network().denom(); let address = AccountId::from_str(address)?; - let amount = amount.into_backend_coin(state.read().await.current_network().denom())?; - let result = nymd_client!(state) - .send(&address, vec![amount.clone()], memo, fee) + let from_address = nymd_client!(state).address().to_string(); + let amount2 = amount.clone().into(); + log::info!( + ">>> Send: amount = {}, minor_amount = {:?}, from = {}, to = {}, fee = {:?}", + amount, + amount2, + from_address, + address.as_ref(), + fee, + ); + let raw_res = nymd_client!(state) + .send(&address, vec![amount2], memo, fee) .await?; - Ok(TauriTxResult::new( - result, + log::info!("<<< tx hash = {}", raw_res.hash.to_string()); + let res = SendTxResult::new( + raw_res, TransactionDetails { - from_address: nymd_client!(state).address().to_string(), + from_address, to_address: address.to_string(), - amount: amount.into(), + amount, }, - )) + denom_minor.as_ref(), + )?; + log::trace!("<<< {:?}", res); + Ok(res) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/admin.rs b/nym-wallet/src-tauri/src/operations/simulate/admin.rs index 01aac84999..e3da08f3cc 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/admin.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/admin.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::BackendError; -use crate::mixnet::admin::TauriContractStateParams; -use crate::simulate::{FeeDetails, SimulateResult}; +use crate::operations::simulate::{FeeDetails, SimulateResult}; use crate::State; use mixnet_contract_common::{ContractStateParams, ExecuteMsg}; +use nym_wallet_types::admin::TauriContractStateParams; use std::sync::Arc; use tokio::sync::RwLock; @@ -28,5 +28,5 @@ pub async fn simulate_update_contract_settings( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/cosmos.rs b/nym-wallet/src-tauri/src/operations/simulate/cosmos.rs index 4f2078713a..4b90e96ac2 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/cosmos.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/cosmos.rs @@ -1,10 +1,10 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::coin::Coin; use crate::error::BackendError; use crate::operations::simulate::{FeeDetails, SimulateResult}; use crate::state::State; +use nym_types::currency::MajorCurrencyAmount; use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; @@ -13,13 +13,13 @@ use validator_client::nymd::{AccountId, MsgSend}; #[tauri::command] pub async fn simulate_send( address: &str, - amount: Coin, + amount: MajorCurrencyAmount, state: tauri::State<'_, Arc>>, ) -> Result { let guard = state.read().await; let to_address = AccountId::from_str(address)?; - let amount = amount.into_backend_coin(guard.current_network().denom())?; + let amount = vec![amount.into()]; let client = guard.current_client()?; let from_address = client.nymd.address().clone(); @@ -29,9 +29,9 @@ pub async fn simulate_send( let msg = MsgSend { from_address, to_address, - amount: vec![amount.into()], + amount, }; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index 1e443cd709..3ee7d18f75 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -1,25 +1,25 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::coin::Coin; use crate::error::BackendError; use crate::nymd_client; -use crate::simulate::{FeeDetails, SimulateResult}; +use crate::operations::simulate::{FeeDetails, SimulateResult}; use crate::State; use mixnet_contract_common::IdentityKey; use mixnet_contract_common::{ExecuteMsg, Gateway, MixNode}; +use nym_types::currency::MajorCurrencyAmount; use std::sync::Arc; use tokio::sync::RwLock; #[tauri::command] pub async fn simulate_bond_gateway( gateway: Gateway, - pledge: Coin, + pledge: MajorCurrencyAmount, owner_signature: String, state: tauri::State<'_, Arc>>, ) -> Result { let guard = state.read().await; - let pledge = pledge.into_backend_coin(guard.current_network().denom())?; + let pledge = pledge.into(); let client = guard.current_client()?; let mixnet_contract = client.nymd.mixnet_contract_address(); @@ -36,7 +36,7 @@ pub async fn simulate_bond_gateway( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -56,18 +56,18 @@ pub async fn simulate_unbond_gateway( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_bond_mixnode( mixnode: MixNode, owner_signature: String, - pledge: Coin, + pledge: MajorCurrencyAmount, state: tauri::State<'_, Arc>>, ) -> Result { let guard = state.read().await; - let pledge = pledge.into_backend_coin(guard.current_network().denom())?; + let pledge = pledge.into(); let client = guard.current_client()?; let mixnet_contract = client.nymd.mixnet_contract_address(); @@ -83,7 +83,7 @@ pub async fn simulate_bond_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -103,7 +103,7 @@ pub async fn simulate_unbond_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -126,17 +126,17 @@ pub async fn simulate_update_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_delegate_to_mixnode( identity: &str, - amount: Coin, + amount: MajorCurrencyAmount, state: tauri::State<'_, Arc>>, ) -> Result { let guard = state.read().await; - let delegation = amount.into_backend_coin(guard.current_network().denom())?; + let delegation = amount.into(); let client = guard.current_client()?; let mixnet_contract = client.nymd.mixnet_contract_address(); @@ -151,7 +151,7 @@ pub async fn simulate_delegate_to_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -174,7 +174,7 @@ pub async fn simulate_undelegate_from_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -185,7 +185,7 @@ pub async fn simulate_claim_operator_reward( .simulate_claim_operator_reward(None) .await?; let gas_price = nymd_client!(state).gas_price().clone(); - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -196,7 +196,7 @@ pub async fn simulate_compound_operator_reward( .simulate_compound_operator_reward(None) .await?; let gas_price = nymd_client!(state).gas_price().clone(); - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -208,7 +208,7 @@ pub async fn simulate_claim_delegator_reward( .simulate_claim_delegator_reward(mix_identity, None) .await?; let gas_price = nymd_client!(state).gas_price().clone(); - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -220,5 +220,5 @@ pub async fn simulate_compound_delegator_reward( .simulate_compound_delegator_reward(mix_identity, None) .await?; let gas_price = nymd_client!(state).gas_price().clone(); - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/mod.rs b/nym-wallet/src-tauri/src/operations/simulate/mod.rs index 273f3d05ab..f9fb86abcf 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mod.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mod.rs @@ -1,8 +1,9 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::coin::Coin; -use serde::{Deserialize, Serialize}; +use nym_types::currency::MajorCurrencyAmount; +use nym_types::error::TypesError; +use nym_types::fees::FeeDetails; use validator_client::nymd::cosmwasm_client::types::GasInfo; use validator_client::nymd::{tx, CosmosCoin, Fee, GasPrice}; @@ -28,21 +29,16 @@ impl SimulateResult { gas_price, } } -} -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FeeDetails { - // expected to be used by the wallet in order to display detailed fee information to the user - pub amount: Option, - pub fee: Fee, -} - -impl SimulateResult { - pub fn detailed_fee(&self) -> FeeDetails { - FeeDetails { - amount: self.to_fee_amount().map(Into::into), + pub fn detailed_fee(&self) -> Result { + let amount: Option = match self.to_fee_amount() { + Some(a) => Some(a.into()), + None => None, + }; + Ok(FeeDetails { + amount, fee: self.to_fee(), - } + }) } fn to_fee_amount(&self) -> Option { diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index 616d58f1e5..2fe52bcba5 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -1,13 +1,13 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::coin::Coin; use crate::error::BackendError; use crate::nymd_client; -use crate::simulate::{FeeDetails, SimulateResult}; +use crate::operations::simulate::{FeeDetails, SimulateResult}; use crate::State; use mixnet_contract_common::IdentityKey; use mixnet_contract_common::{Gateway, MixNode}; +use nym_types::currency::MajorCurrencyAmount; use std::sync::Arc; use tokio::sync::RwLock; use vesting_contract_common::ExecuteMsg; @@ -15,12 +15,12 @@ use vesting_contract_common::ExecuteMsg; #[tauri::command] pub async fn simulate_vesting_bond_gateway( gateway: Gateway, - pledge: Coin, + pledge: MajorCurrencyAmount, owner_signature: String, state: tauri::State<'_, Arc>>, ) -> Result { let guard = state.read().await; - let pledge = pledge.into_backend_coin(guard.current_network().denom())?; + let pledge = pledge.into(); let client = guard.current_client()?; let vesting_contract = client.nymd.vesting_contract_address(); @@ -31,13 +31,13 @@ pub async fn simulate_vesting_bond_gateway( &ExecuteMsg::BondGateway { gateway, owner_signature, - amount: pledge.into(), + amount: pledge, }, vec![], )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -57,18 +57,18 @@ pub async fn simulate_vesting_unbond_gateway( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_vesting_bond_mixnode( mixnode: MixNode, owner_signature: String, - pledge: Coin, + pledge: MajorCurrencyAmount, state: tauri::State<'_, Arc>>, ) -> Result { let guard = state.read().await; - let pledge = pledge.into_backend_coin(guard.current_network().denom())?; + let pledge = pledge.into(); let client = guard.current_client()?; let vesting_contract = client.nymd.vesting_contract_address(); @@ -79,13 +79,13 @@ pub async fn simulate_vesting_bond_mixnode( &ExecuteMsg::BondMixnode { mix_node: mixnode, owner_signature, - amount: pledge.into(), + amount: pledge, }, vec![], )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -105,7 +105,7 @@ pub async fn simulate_vesting_unbond_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -128,16 +128,16 @@ pub async fn simulate_vesting_update_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] pub async fn simulate_withdraw_vested_coins( - amount: Coin, + amount: MajorCurrencyAmount, state: tauri::State<'_, Arc>>, ) -> Result { let guard = state.read().await; - let amount = amount.into_backend_coin(guard.current_network().denom())?; + let amount = amount.into(); let client = guard.current_client()?; let vesting_contract = client.nymd.vesting_contract_address(); @@ -145,14 +145,12 @@ pub async fn simulate_withdraw_vested_coins( let msg = client.nymd.wrap_contract_execute_message( vesting_contract, - &ExecuteMsg::WithdrawVestedCoins { - amount: amount.into(), - }, + &ExecuteMsg::WithdrawVestedCoins { amount }, vec![], )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -163,7 +161,7 @@ pub async fn simulate_vesting_claim_operator_reward( .simulate_vesting_claim_operator_reward(None) .await?; let gas_price = nymd_client!(state).gas_price().clone(); - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -174,7 +172,7 @@ pub async fn simulate_vesting_compound_operator_reward( .simulate_vesting_compound_operator_reward(None) .await?; let gas_price = nymd_client!(state).gas_price().clone(); - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -186,7 +184,7 @@ pub async fn simulate_vesting_claim_delegator_reward( .simulate_vesting_claim_delegator_reward(mix_identity, None) .await?; let gas_price = nymd_client!(state).gas_price().clone(); - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } #[tauri::command] @@ -198,5 +196,5 @@ pub async fn simulate_vesting_compound_delegator_reward( .simulate_vesting_compound_delegator_reward(mix_identity, None) .await?; let gas_price = nymd_client!(state).gas_price().clone(); - Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()) + Ok(SimulateResult::new(result.gas_info, gas_price).detailed_fee()?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index d06842f067..67df1a2e6d 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -1,8 +1,10 @@ -use crate::coin::Coin; use crate::error::BackendError; use crate::nymd_client; use crate::state::State; use crate::{Gateway, MixNode}; + +use nym_types::currency::MajorCurrencyAmount; +use nym_types::transaction::TransactionExecuteResult; use std::sync::Arc; use tokio::sync::RwLock; use validator_client::nymd::{Fee, VestingSigningClient}; @@ -10,62 +12,120 @@ use validator_client::nymd::{Fee, VestingSigningClient}; #[tauri::command] pub async fn vesting_bond_gateway( gateway: Gateway, - pledge: Coin, + pledge: MajorCurrencyAmount, owner_signature: String, fee: Option, state: tauri::State<'_, Arc>>, -) -> Result<(), BackendError> { - let pledge = pledge.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .vesting_bond_gateway(gateway, &owner_signature, pledge, fee) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + let pledge_minor = pledge.clone().into(); + log::info!( + ">>> Bond gateway with locked tokens: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", + gateway.identity_key, + pledge, + pledge_minor, + fee, + ); + let res = nymd_client!(state) + .vesting_bond_gateway(gateway, &owner_signature, pledge_minor, fee) .await?; - Ok(()) + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn vesting_unbond_gateway( fee: Option, state: tauri::State<'_, Arc>>, -) -> Result<(), BackendError> { - nymd_client!(state).vesting_unbond_gateway(fee).await?; - Ok(()) -} - -#[tauri::command] -pub async fn vesting_unbond_mixnode( - fee: Option, - state: tauri::State<'_, Arc>>, -) -> Result<(), BackendError> { - nymd_client!(state).vesting_unbond_mixnode(fee).await?; - Ok(()) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Unbond gateway bonded with locked tokens, fee = {:?}", + fee + ); + let res = nymd_client!(state).vesting_unbond_gateway(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn vesting_bond_mixnode( mixnode: MixNode, owner_signature: String, - pledge: Coin, + pledge: MajorCurrencyAmount, fee: Option, state: tauri::State<'_, Arc>>, -) -> Result<(), BackendError> { - let pledge = pledge.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .vesting_bond_mixnode(mixnode, &owner_signature, pledge, fee) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + let pledge_minor = pledge.clone().into(); + log::info!( + ">>> Bond mixnode with locked tokens: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", + mixnode.identity_key, + pledge, + pledge_minor, + fee + ); + let res = nymd_client!(state) + .vesting_bond_mixnode(mixnode, &owner_signature, pledge_minor, fee) .await?; - Ok(()) + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) +} + +#[tauri::command] +pub async fn vesting_unbond_mixnode( + fee: Option, + state: tauri::State<'_, Arc>>, +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Unbond mixnode bonded with locked tokens, fee = {:?}", + fee + ); + let res = nymd_client!(state).vesting_unbond_mixnode(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] pub async fn withdraw_vested_coins( - amount: Coin, + amount: MajorCurrencyAmount, fee: Option, state: tauri::State<'_, Arc>>, -) -> Result<(), BackendError> { - let amount = amount.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .withdraw_vested_coins(amount, fee) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + let amount_minor = amount.clone().into(); + log::info!( + ">>> Withdraw vested liquid coins: amount = {}, amount_minor = {}, fee = {:?}", + amount, + amount_minor, + fee + ); + let res = nymd_client!(state) + .withdraw_vested_coins(amount_minor, fee) .await?; - Ok(()) + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] @@ -73,9 +133,20 @@ pub async fn vesting_update_mixnode( profit_margin_percent: u8, fee: Option, state: tauri::State<'_, Arc>>, -) -> Result<(), BackendError> { - nymd_client!(state) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Update mixnode bonded with locked tokens: profit_margin_percent = {}, fee = {:?}", + profit_margin_percent, + fee, + ); + let res = nymd_client!(state) .vesting_update_mixnode_config(profit_margin_percent, fee) .await?; - Ok(()) + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index e6bb223787..321fa69a1b 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -1,48 +1,67 @@ -use crate::coin::Coin; +use std::sync::Arc; + +use tokio::sync::RwLock; + +use nym_types::currency::MajorCurrencyAmount; +use nym_types::delegation::{from_contract_delegation_events, DelegationEvent}; +use nym_types::transaction::TransactionExecuteResult; +use validator_client::nymd::{Fee, VestingSigningClient}; + use crate::error::BackendError; use crate::nymd_client; use crate::state::State; -use crate::utils::DelegationEvent; -use crate::utils::DelegationResult; -use std::sync::Arc; -use tokio::sync::RwLock; -use validator_client::nymd::{Fee, VestingSigningClient}; #[tauri::command] pub async fn get_pending_vesting_delegation_events( state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { + log::info!(">>> Get pending delegations from vesting contract"); + let guard = state.read().await; let client = &guard.current_client()?.nymd; let vesting_contract = client.vesting_contract_address(); - Ok(client + let events = client .get_pending_delegation_events( client.address().to_string(), Some(vesting_contract.to_string()), ) - .await? - .into_iter() - .map(|delegation_event| delegation_event.into()) - .collect::>()) + .await?; + + log::info!("<<< {} events", events.len()); + log::trace!("<<< {:?}", events); + + match from_contract_delegation_events(events) { + Ok(res) => Ok(res), + Err(e) => Err(e.into()), + } } #[tauri::command] pub async fn vesting_delegate_to_mixnode( identity: &str, - amount: Coin, + amount: MajorCurrencyAmount, fee: Option, state: tauri::State<'_, Arc>>, -) -> Result { - let delegation = amount.into_backend_coin(state.read().await.current_network().denom())?; - nymd_client!(state) - .vesting_delegate_to_mixnode(identity, delegation.clone(), fee) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + let delegation = amount.clone().into(); + log::info!( + ">>> Delegate to mixnode with locked tokens: identity_key = {}, amount = {}, minor_amount = {}, fee = {:?}", + identity, + amount, + delegation, + fee + ); + let res = nymd_client!(state) + .vesting_delegate_to_mixnode(identity, delegation, fee) .await?; - Ok(DelegationResult::new( - nymd_client!(state).address().as_ref(), - identity, - Some(delegation.into()), - )) + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } #[tauri::command] @@ -50,13 +69,20 @@ pub async fn vesting_undelegate_from_mixnode( identity: &str, fee: Option, state: tauri::State<'_, Arc>>, -) -> Result { - nymd_client!(state) +) -> Result { + let denom_minor = state.read().await.current_network().denom(); + log::info!( + ">>> Undelegate from mixnode delegated with locked tokens: identity_key = {}, fee = {:?}", + identity, + fee, + ); + let res = nymd_client!(state) .vesting_undelegate_from_mixnode(identity, fee) .await?; - Ok(DelegationResult::new( - nymd_client!(state).address().as_ref(), - identity, - None, - )) + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, + denom_minor.as_ref(), + )?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/mod.rs b/nym-wallet/src-tauri/src/operations/vesting/mod.rs index f680e5ec6d..fb80d4ed43 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/mod.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/mod.rs @@ -1,103 +1,4 @@ -use crate::coin::Coin; -use serde::{Deserialize, Serialize}; -use vesting_contract::vesting::Account as VestingAccount; -use vesting_contract::vesting::VestingPeriod as VestingVestingPeriod; -use vesting_contract_common::OriginalVestingResponse as VestingOriginalVestingResponse; -use vesting_contract_common::PledgeData as VestingPledgeData; - pub mod bond; pub mod delegate; pub mod queries; pub mod rewards; - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/pledgedata.ts"))] -#[derive(Serialize, Deserialize, Debug)] -pub struct PledgeData { - pub amount: Coin, - pub block_time: u64, -} - -impl From for PledgeData { - fn from(data: VestingPledgeData) -> Self { - Self { - amount: data.amount().into(), - block_time: data.block_time().seconds(), - } - } -} - -impl PledgeData { - fn and_then(data: VestingPledgeData) -> Option { - Some(data.into()) - } -} - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr( - test, - ts(export, export_to = "../src/types/rust/originalvestingresponse.ts") -)] -#[derive(Serialize, Deserialize, Debug)] -pub struct OriginalVestingResponse { - amount: Coin, - number_of_periods: usize, - period_duration: u64, -} - -impl From for OriginalVestingResponse { - fn from(data: VestingOriginalVestingResponse) -> Self { - Self { - amount: data.amount().into(), - number_of_periods: data.number_of_periods(), - period_duration: data.period_duration(), - } - } -} - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr( - test, - ts(export, export_to = "../src/types/rust/vestingaccountinfo.ts") -)] -#[derive(Serialize, Deserialize, Debug)] -pub struct VestingAccountInfo { - owner_address: String, - staking_address: Option, - start_time: u64, - periods: Vec, - coin: Coin, -} - -impl From for VestingAccountInfo { - fn from(account: VestingAccount) -> Self { - let mut periods = Vec::new(); - for period in account.periods() { - periods.push(period.into()); - } - Self { - owner_address: account.owner_address().to_string(), - staking_address: account.staking_address().map(|a| a.to_string()), - start_time: account.start_time().seconds(), - periods, - coin: account.coin().into(), - } - } -} - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/vestingperiod.ts"))] -#[derive(Serialize, Deserialize, Debug)] -pub struct VestingPeriod { - start_time: u64, - period_seconds: u64, -} - -impl From for VestingPeriod { - fn from(period: VestingVestingPeriod) -> Self { - Self { - start_time: period.start_time, - period_seconds: period.period_seconds, - } - } -} diff --git a/nym-wallet/src-tauri/src/operations/vesting/queries.rs b/nym-wallet/src-tauri/src/operations/vesting/queries.rs index 1e1b38f5cc..72c85f69d7 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/queries.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/queries.rs @@ -1,42 +1,50 @@ -use super::VestingAccountInfo; -use crate::coin::Coin; -use crate::error::BackendError; -use crate::nymd_client; -use crate::state::State; -use cosmwasm_std::Timestamp; use std::sync::Arc; + +use cosmwasm_std::Timestamp; use tokio::sync::RwLock; + +use nym_types::currency::MajorCurrencyAmount; +use nym_types::vesting::VestingAccountInfo; +use nym_types::vesting::{OriginalVestingResponse, PledgeData}; use validator_client::nymd::VestingQueryClient; use vesting_contract_common::Period; -use super::{OriginalVestingResponse, PledgeData}; +use crate::error::BackendError; +use crate::nymd_client; +use crate::state::State; #[tauri::command] pub async fn locked_coins( block_time: Option, state: tauri::State<'_, Arc>>, -) -> Result { - Ok(nymd_client!(state) +) -> Result { + log::info!(">>> Query locked coins"); + let res = nymd_client!(state) .locked_coins( nymd_client!(state).address().as_ref(), block_time.map(Timestamp::from_seconds), ) .await? - .into()) + .into(); + log::info!("<<< locked coins = {}", res); + Ok(res) } #[tauri::command] pub async fn spendable_coins( block_time: Option, state: tauri::State<'_, Arc>>, -) -> Result { - Ok(nymd_client!(state) +) -> Result { + log::info!(">>> Query spendable coins"); + let res = nymd_client!(state) .spendable_coins( nymd_client!(state).address().as_ref(), block_time.map(Timestamp::from_seconds), ) .await? - .into()) + .into(); + log::info!("<<< spendable coins = {}", res); + Ok(res) } #[tauri::command] @@ -44,14 +52,17 @@ pub async fn vested_coins( vesting_account_address: &str, block_time: Option, state: tauri::State<'_, Arc>>, -) -> Result { - Ok(nymd_client!(state) +) -> Result { + log::info!(">>> Query vested coins"); + let res = nymd_client!(state) .vested_coins( vesting_account_address, block_time.map(Timestamp::from_seconds), ) .await? - .into()) + .into(); + log::info!("<<< vested coins = {}", res); + Ok(res) } #[tauri::command] @@ -59,14 +70,17 @@ pub async fn vesting_coins( vesting_account_address: &str, block_time: Option, state: tauri::State<'_, Arc>>, -) -> Result { - Ok(nymd_client!(state) +) -> Result { + log::info!(">>> Query vesting coins"); + let res = nymd_client!(state) .vesting_coins( vesting_account_address, block_time.map(Timestamp::from_seconds), ) .await? - .into()) + .into(); + log::info!("<<< vesting coins = {}", res); + Ok(res) } #[tauri::command] @@ -74,10 +88,13 @@ pub async fn vesting_start_time( vesting_account_address: &str, state: tauri::State<'_, Arc>>, ) -> Result { - Ok(nymd_client!(state) + log::info!(">>> Query vesting start time"); + let res = nymd_client!(state) .vesting_start_time(vesting_account_address) .await? - .seconds()) + .seconds(); + log::info!("<<< vesting start time = {}", res); + Ok(res) } #[tauri::command] @@ -85,10 +102,13 @@ pub async fn vesting_end_time( vesting_account_address: &str, state: tauri::State<'_, Arc>>, ) -> Result { - Ok(nymd_client!(state) + log::info!(">>> Query vesting end time"); + let res = nymd_client!(state) .vesting_end_time(vesting_account_address) .await? - .seconds()) + .seconds(); + log::info!("<<< vesting end time = {}", res); + Ok(res) } #[tauri::command] @@ -96,10 +116,13 @@ pub async fn original_vesting( vesting_account_address: &str, state: tauri::State<'_, Arc>>, ) -> Result { - Ok(nymd_client!(state) + log::info!(">>> Query original vesting"); + let res = nymd_client!(state) .original_vesting(vesting_account_address) .await? - .into()) + .try_into()?; + log::info!("<<< {:?}", res); + Ok(res) } #[tauri::command] @@ -107,29 +130,36 @@ pub async fn delegated_free( vesting_account_address: &str, block_time: Option, state: tauri::State<'_, Arc>>, -) -> Result { - Ok(nymd_client!(state) +) -> Result { + log::info!(">>> Query delegated free"); + let res = nymd_client!(state) .delegated_free( vesting_account_address, block_time.map(Timestamp::from_seconds), ) .await? - .into()) + .into(); + log::info!("<<< delegated free = {}", res); + Ok(res) } +/// Returns the total amount of delegated tokens that have vested #[tauri::command] pub async fn delegated_vesting( block_time: Option, vesting_account_address: &str, state: tauri::State<'_, Arc>>, -) -> Result { - Ok(nymd_client!(state) +) -> Result { + log::info!(">>> Query delegated vesting"); + let res = nymd_client!(state) .delegated_vesting( vesting_account_address, block_time.map(Timestamp::from_seconds), ) .await? - .into()) + .into(); + log::info!("<<< delegated_vesting = {}", res); + Ok(res) } #[tauri::command] @@ -137,10 +167,13 @@ pub async fn vesting_get_mixnode_pledge( address: &str, state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - Ok(nymd_client!(state) + log::info!(">>> Query vesting get mixnode pledge"); + let res = nymd_client!(state) .get_mixnode_pledge(address) .await? - .and_then(PledgeData::and_then)) + .and_then(PledgeData::and_then); + log::info!("<<< {:?}", res); + Ok(res) } #[tauri::command] @@ -148,10 +181,13 @@ pub async fn vesting_get_gateway_pledge( address: &str, state: tauri::State<'_, Arc>>, ) -> Result, BackendError> { - Ok(nymd_client!(state) + log::info!(">>> Query vesting get gateway pledge"); + let res = nymd_client!(state) .get_gateway_pledge(address) .await? - .and_then(PledgeData::and_then)) + .and_then(PledgeData::and_then); + log::info!("<<< {:?}", res); + Ok(res) } #[tauri::command] @@ -159,9 +195,12 @@ pub async fn get_current_vesting_period( address: &str, state: tauri::State<'_, Arc>>, ) -> Result { - Ok(nymd_client!(state) + log::info!(">>> Query current vesting period"); + let res = nymd_client!(state) .get_current_vesting_period(address) - .await?) + .await?; + log::info!("<<< {:?}", res); + Ok(res) } #[tauri::command] @@ -169,5 +208,8 @@ pub async fn get_account_info( address: &str, state: tauri::State<'_, Arc>>, ) -> Result { - Ok(nymd_client!(state).get_account(address).await?.into()) + log::info!(">>> Query account info"); + let res = nymd_client!(state).get_account(address).await?.try_into()?; + log::info!("<<< {:?}", res); + Ok(res) } diff --git a/nym-wallet/src-tauri/src/state.rs b/nym-wallet/src-tauri/src/state.rs index 2fee2b09f0..1095d872b6 100644 --- a/nym-wallet/src-tauri/src/state.rs +++ b/nym-wallet/src-tauri/src/state.rs @@ -1,6 +1,7 @@ +use crate::config; use crate::error::BackendError; -use crate::network::Network; -use crate::{config, network_config}; +use nym_wallet_types::network::Network; +use nym_wallet_types::network_config; use strum::IntoEnumIterator; use validator_client::nymd::{AccountId as CosmosAccountId, SigningNymdClient}; diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index 8bb1982d26..212bc7ad6a 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -1,23 +1,12 @@ -use crate::coin::{Coin, Denom}; use crate::error::BackendError; use crate::nymd_client; use crate::state::State; -use mixnet_contract_common::mixnode::DelegationEvent as ContractDelegationEvent; -use mixnet_contract_common::mixnode::PendingUndelegate as ContractPendingUndelegate; -use mixnet_contract_common::Delegation; +use nym_types::currency::MajorCurrencyAmount; +use nym_wallet_types::app::AppEnv; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::RwLock; -use validator_client::nymd::{tx, CosmosCoin, Gas, GasPrice}; - -#[allow(non_snake_case)] -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/appEnv.ts"))] -#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] -pub struct AppEnv { - pub ADMIN_ADDRESS: Option, - pub SHOW_TERMINAL: Option, -} +use validator_client::nymd::{tx, Coin, CosmosCoin, Gas, GasPrice}; fn get_env_as_option(key: &str) -> Option { match ::std::env::var(key) { @@ -34,18 +23,6 @@ pub fn get_env() -> AppEnv { } } -#[tauri::command] -pub fn major_to_minor(amount: &str) -> Coin { - let coin = Coin::new(amount, &Denom::Major); - coin.to_minor() -} - -#[tauri::command] -pub fn minor_to_major(amount: &str) -> Coin { - let coin = Coin::new(amount, &Denom::Minor); - coin.to_major() -} - #[tauri::command] pub async fn owns_mixnode( state: tauri::State<'_, Arc>>, @@ -169,86 +146,11 @@ impl Operation { pub async fn get_old_and_incorrect_hardcoded_fee( state: tauri::State<'_, Arc>>, operation: Operation, -) -> Result { +) -> Result { let mut approximate_fee = operation.default_fee(nymd_client!(state).gas_price()); // on all our chains it should only ever contain a single type of currency assert_eq!(approximate_fee.amount.len(), 1); let coin: Coin = approximate_fee.amount.pop().unwrap().into(); log::info!("hardcoded fee for {:?} is {:?}", operation, coin); - Ok(coin.to_major()) -} - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/delegationresult.ts"))] -#[derive(Serialize, Deserialize)] -pub struct DelegationResult { - source_address: String, - target_address: String, - amount: Option, -} - -impl DelegationResult { - pub fn new( - source_address: &str, - target_address: &str, - amount: Option, - ) -> DelegationResult { - DelegationResult { - source_address: source_address.to_string(), - target_address: target_address.to_string(), - amount, - } - } -} - -impl From for DelegationResult { - fn from(delegation: Delegation) -> Self { - DelegationResult { - source_address: delegation.owner().to_string(), - target_address: delegation.node_identity(), - amount: Some(delegation.amount.into()), - } - } -} - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/delegationevent.ts"))] -#[derive(Deserialize, Serialize)] -pub enum DelegationEvent { - Delegate(DelegationResult), - Undelegate(PendingUndelegate), -} - -impl From for DelegationEvent { - fn from(event: ContractDelegationEvent) -> Self { - match event { - ContractDelegationEvent::Delegate(delegation) => { - DelegationEvent::Delegate(delegation.into()) - } - ContractDelegationEvent::Undelegate(pending_undelegate) => { - DelegationEvent::Undelegate(pending_undelegate.into()) - } - } - } -} - -#[cfg_attr(test, derive(ts_rs::TS))] -#[cfg_attr(test, ts(export, export_to = "../src/types/rust/pendingundelegate.ts"))] -#[derive(Deserialize, Serialize)] -pub struct PendingUndelegate { - mix_identity: String, - delegate: String, - proxy: Option, - block_height: u64, -} - -impl From for PendingUndelegate { - fn from(pending_undelegate: ContractPendingUndelegate) -> Self { - PendingUndelegate { - mix_identity: pending_undelegate.mix_identity(), - delegate: pending_undelegate.delegate().to_string(), - proxy: pending_undelegate.proxy().map(|p| p.to_string()), - block_height: pending_undelegate.block_height(), - } - } + Ok(coin.into()) } diff --git a/nym-wallet/src/components/Accounts/AccountOverview.tsx b/nym-wallet/src/components/Accounts/AccountOverview.tsx index 17830ba319..fe8b4778be 100644 --- a/nym-wallet/src/components/Accounts/AccountOverview.tsx +++ b/nym-wallet/src/components/Accounts/AccountOverview.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Button } from '@mui/material'; -import { AccountEntry } from 'src/types'; +import { AccountEntry } from '@nymproject/types'; import { AccountAvatar } from './AccountAvatar'; export const AccountOverview = ({ account, onClick }: { account: AccountEntry; onClick: () => void }) => ( diff --git a/nym-wallet/src/components/AppBar.tsx b/nym-wallet/src/components/AppBar.tsx index def6dce2f5..47d9127c26 100644 --- a/nym-wallet/src/components/AppBar.tsx +++ b/nym-wallet/src/components/AppBar.tsx @@ -1,17 +1,16 @@ import React, { useContext } from 'react'; import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { Logout } from '@mui/icons-material'; import TerminalIcon from '@mui/icons-material/Terminal'; import { AppContext } from '../context/main'; import { NetworkSelector } from './NetworkSelector'; -import { Node as NodeIcon } from '../svg-icons/node'; import { MultiAccounts } from './Accounts'; -import { config } from '../../config'; +import { config } from '../config'; export const AppBar = () => { - const { showSettings, logOut, handleShowSettings, handleShowTerminal, appEnv } = useContext(AppContext); - const history = useHistory(); + const { logOut, handleShowTerminal, appEnv } = useContext(AppContext); + const navigate = useNavigate(); return ( @@ -32,21 +31,13 @@ export const AppBar = () => { )} - - - - - + { await logOut(); - history.push('/'); + navigate('/'); }} sx={{ color: 'nym.background.dark' }} > diff --git a/nym-wallet/src/components/Delegation/DelegateModal.tsx b/nym-wallet/src/components/Delegation/DelegateModal.tsx new file mode 100644 index 0000000000..1da4668c64 --- /dev/null +++ b/nym-wallet/src/components/Delegation/DelegateModal.tsx @@ -0,0 +1,172 @@ +import React, { useState } from 'react'; +import { Box, Stack, Typography } from '@mui/material'; +import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { CurrencyDenom, MajorCurrencyAmount } from '@nymproject/types'; +import { getGasFee } from 'src/requests'; +import { SimpleModal } from '../Modals/SimpleModal'; +import { ModalListItem } from './ModalListItem'; +import { validateKey } from '../../utils'; +import { TokenPoolSelector, TPoolOption } from '../TokenPoolSelector'; + +const MIN_AMOUNT_TO_DELEGATE = 10; + +export const DelegateModal: React.FC<{ + open: boolean; + onClose?: () => void; + onOk?: (identityKey: string, amount: MajorCurrencyAmount, tokenPool: TPoolOption) => Promise; + identityKey?: string; + onIdentityKeyChanged?: (identityKey: string) => void; + onAmountChanged?: (amount: string) => void; + header?: string; + buttonText?: string; + rewardInterval: string; + accountBalance?: string; + estimatedReward?: number; + profitMarginPercentage?: number | null; + nodeUptimePercentage?: number | null; + feeOverride?: string; + currency: CurrencyDenom; + initialAmount?: string; + hasVestingContract: boolean; +}> = ({ + open, + onIdentityKeyChanged, + onAmountChanged, + onClose, + onOk, + header, + buttonText, + identityKey: initialIdentityKey, + rewardInterval, + accountBalance, + feeOverride, + estimatedReward, + currency, + profitMarginPercentage, + nodeUptimePercentage, + initialAmount, + hasVestingContract, +}) => { + const [identityKey, setIdentityKey] = useState(initialIdentityKey); + const [amount, setAmount] = useState(initialAmount); + const [isValidated, setValidated] = useState(false); + const [errorAmount, setErrorAmount] = useState(); + const [tokenPool, setTokenPool] = useState('balance'); + const [fee, setFee] = useState(); + + const getFee = async () => { + if (feeOverride) setFee(feeOverride); + else { + const res = await getGasFee('BondMixnode'); + setFee(res.amount); + } + }; + + const validate = () => { + let newValidatedValue = true; + if (!identityKey || !validateKey(identityKey, 32)) { + newValidatedValue = false; + } + if (amount && Number(amount) < MIN_AMOUNT_TO_DELEGATE) { + setErrorAmount(`Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${currency}`); + newValidatedValue = false; + } else { + setErrorAmount(undefined); + } + setValidated(newValidatedValue); + }; + + const handleOk = () => { + if (onOk && amount && identityKey) { + onOk(identityKey, { amount, denom: currency }, tokenPool); + } + }; + + const handleIdentityKeyChanged = (newIdentityKey: string) => { + setIdentityKey(newIdentityKey); + if (onIdentityKeyChanged) { + onIdentityKeyChanged(newIdentityKey); + } + }; + + const handleAmountChanged = (newAmount: MajorCurrencyAmount) => { + setAmount(newAmount.amount); + if (onAmountChanged) { + onAmountChanged(newAmount.amount); + } + }; + + React.useEffect(() => { + validate(); + }, [amount, identityKey]); + + React.useEffect(() => { + getFee(); + }, []); + + return ( + + + + {hasVestingContract && setTokenPool(pool)} />} + + + + {errorAmount} + + + Account balance + {accountBalance} + + + ); +}; diff --git a/nym-wallet/src/components/Delegation/DelegationActions.stories.tsx b/nym-wallet/src/components/Delegation/DelegationActions.stories.tsx new file mode 100644 index 0000000000..29d29fe4ac --- /dev/null +++ b/nym-wallet/src/components/Delegation/DelegationActions.stories.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { ComponentMeta } from '@storybook/react'; + +import { DelegationActions } from './DelegationActions'; + +export default { + title: 'Delegation/Components/Delegation List Item Actions', + component: DelegationActions, +} as ComponentMeta; + +export const Default = () => ; + +export const RedeemingDisabled = () => ; + +export const PendingDelegation = () => ; + +export const PendingUndelegation = () => ( + +); diff --git a/nym-wallet/src/components/Delegation/DelegationActions.tsx b/nym-wallet/src/components/Delegation/DelegationActions.tsx new file mode 100644 index 0000000000..2833a2ecde --- /dev/null +++ b/nym-wallet/src/components/Delegation/DelegationActions.tsx @@ -0,0 +1,163 @@ +import React from 'react'; +import { + Box, + Button, + IconButton, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + Stack, + Tooltip, + Typography, +} from '@mui/material'; +import { MoreVertSharp } from '@mui/icons-material'; +import { DelegationEventKind } from '@nymproject/types'; +import { Delegate, Undelegate } from '../../svg-icons'; +import { DelegateListItemPending } from './types'; + +export type DelegationListItemActions = 'delegate' | 'undelegate' | 'redeem' | 'compound'; + +const BUTTON_SIZE = '32px'; +const MIN_WIDTH = '150px'; + +export const DelegationActions: React.FC<{ + onActionClick?: (action: DelegationListItemActions) => void; + isPending?: DelegateListItemPending; + disableRedeemingRewards?: boolean; +}> = ({ disableRedeemingRewards, onActionClick, isPending }) => { + if (isPending) { + return ( + + + + Pending {isPending.actionType === 'delegate' ? 'delegation' : 'undelegation'}... + + + + ); + } + return ( + + + + + + + + + + + + + + ); +}; + +const DelegationActionsMenuItem = ({ + title, + description, + onClick, + Icon, + disabled, +}: { + title: string; + description?: string; + onClick?: () => void; + Icon?: React.ReactNode; + disabled?: boolean; +}) => ( + + {Icon} + + +); + +export const DelegationsActionsMenu: React.FC<{ + onActionClick?: (action: DelegationListItemActions) => void; + isPending?: DelegationEventKind; + disableRedeemingRewards?: boolean; + disableDelegateMore?: boolean; +}> = ({ disableRedeemingRewards, disableDelegateMore, onActionClick, isPending }) => { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => setAnchorEl(null); + + const handleActionSelect = (action: DelegationListItemActions) => { + handleClose(); + onActionClick?.(action); + }; + + if (isPending) { + return ( + + + + Pending {isPending === 'Delegate' ? 'delegation' : 'undelegation'}... + + + + ); + } + + return ( + <> + + + + + } + onClick={() => handleActionSelect?.('delegate')} + disabled={disableDelegateMore} + /> + } + onClick={() => handleActionSelect?.('undelegate')} + disabled={false} + /> + R} + onClick={() => handleActionSelect?.('redeem')} + disabled={disableRedeemingRewards} + /> + C} + onClick={() => handleActionSelect?.('compound')} + disabled={disableRedeemingRewards} + /> + + + ); +}; diff --git a/nym-wallet/src/components/Delegation/DelegationList.stories.tsx b/nym-wallet/src/components/Delegation/DelegationList.stories.tsx new file mode 100644 index 0000000000..e60184d190 --- /dev/null +++ b/nym-wallet/src/components/Delegation/DelegationList.stories.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { ComponentMeta } from '@storybook/react'; + +import { DelegationWithEverything } from '@nymproject/types'; +import { DelegationList } from './DelegationList'; + +export default { + title: 'Delegation/Components/Delegation List', + component: DelegationList, +} as ComponentMeta; + +const explorerUrl = 'https://sandbox-explorer.nymtech.net/network-components/mixnodes'; + +export const items: DelegationWithEverything[] = [ + { + node_identity: 'FiojKW7oY9WQmLCiYAsCA21tpowZHS6zcUoyYm319p6Z', + delegated_on_iso_datetime: new Date(2021, 1, 1).toDateString(), + accumulated_rewards: { amount: '0.05', denom: 'NYM' }, + amount: { amount: '10', denom: 'NYM' }, + profit_margin_percent: 0.1122323949234, + owner: '', + block_height: BigInt(100), + stake_saturation: 0.5, + proxy: '', + avg_uptime_percent: 0.5, + total_delegation: { amount: '0', denom: 'NYM' }, + pledge_amount: { amount: '0', denom: 'NYM' }, + pending_events: [], + history: [], + }, + { + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + accumulated_rewards: { amount: '0.1', denom: 'NYM' }, + amount: { amount: '100', denom: 'NYM' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + profit_margin_percent: 0.89, + owner: '', + block_height: BigInt(4000), + stake_saturation: 0.5, + proxy: '', + avg_uptime_percent: 0.1, + total_delegation: { amount: '0', denom: 'NYM' }, + pledge_amount: { amount: '0', denom: 'NYM' }, + pending_events: [], + history: [], + }, +]; + +export const WithData = () => ; + +export const Empty = () => ; + +export const OneItem = () => ; + +export const Loading = () => ; diff --git a/nym-wallet/src/components/Delegation/DelegationList.tsx b/nym-wallet/src/components/Delegation/DelegationList.tsx new file mode 100644 index 0000000000..c7680cbb47 --- /dev/null +++ b/nym-wallet/src/components/Delegation/DelegationList.tsx @@ -0,0 +1,203 @@ +import React from 'react'; +import { + Box, + Chip, + CircularProgress, + Link, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TableSortLabel, + Tooltip, +} from '@mui/material'; +import { visuallyHidden } from '@mui/utils'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; +import { DelegationWithEverything } from '@nymproject/types'; +import { format } from 'date-fns'; +import { DelegationListItemActions, DelegationsActionsMenu } from './DelegationActions'; + +type Order = 'asc' | 'desc'; + +interface EnhancedTableProps { + onRequestSort: (event: React.MouseEvent, property: keyof DelegationWithEverything) => void; + order: Order; + orderBy: string; +} + +interface HeadCell { + id: keyof DelegationWithEverything; + label: string; + sortable: boolean; + disablePadding?: boolean; + align: 'left' | 'center' | 'right'; +} + +const headCells: HeadCell[] = [ + { id: 'node_identity', label: 'Node ID', sortable: true, align: 'left' }, + { id: 'delegated_on_iso_datetime', label: 'Delegated on', sortable: true, align: 'center' }, + { id: 'amount', label: 'Delegation', sortable: true, align: 'center' }, + { id: 'accumulated_rewards', label: 'Reward', sortable: true, align: 'center' }, + { id: 'profit_margin_percent', label: 'Profit margin', sortable: true, align: 'center' }, + { id: 'stake_saturation', label: 'Stake saturation', sortable: true, align: 'center' }, + { id: 'avg_uptime_percent', label: 'Uptime', sortable: true, align: 'center' }, +]; + +function descendingComparator(a: T, b: T, orderBy: keyof T) { + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; +} + +function getComparator( + order: Order, + orderBy: Key, +): (a: DelegationWithEverything, b: DelegationWithEverything) => number { + return order === 'desc' + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +} + +const EnhancedTableHead: React.FC = ({ order, orderBy, onRequestSort }) => { + const createSortHandler = (property: keyof DelegationWithEverything) => (event: React.MouseEvent) => { + onRequestSort(event, property); + }; + + return ( + + + {headCells.map((headCell) => ( + + + {headCell.label} + {orderBy === headCell.id ? ( + + {order === 'desc' ? 'sorted descending' : 'sorted ascending'} + + ) : null} + + + ))} + + + + ); +}; + +export const DelegationList: React.FC<{ + isLoading?: boolean; + items?: DelegationWithEverything[]; + onItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void; + explorerUrl: string; +}> = ({ isLoading, items, onItemActionClick, explorerUrl }) => { + const [order, setOrder] = React.useState('asc'); + const [orderBy, setOrderBy] = React.useState('delegated_on_iso_datetime'); + + const handleRequestSort = (event: React.MouseEvent, property: keyof DelegationWithEverything) => { + const isAsc = orderBy === property && order === 'asc'; + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(property); + }; + + return ( + + + + + {items?.length ? ( + items.sort(getComparator(order, orderBy)).map((item) => ( + + + + Copy identity key {item.node_identity} to clipboard + + } + /> + + Click to view {item.node_identity} in the Network Explorer + + } + placement="right" + arrow + > + + {item.node_identity.slice(0, 6)}...{item.node_identity.slice(-6)} + + + + {format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')} + {`${item.amount.amount} ${item.amount.denom}`} + + {!item.accumulated_rewards + ? '-' + : `${item.accumulated_rewards.amount} ${item.accumulated_rewards.denom}`} + + + {!item.profit_margin_percent ? '-' : `${item.profit_margin_percent}%`} + + + {!item.stake_saturation ? '-' : `${Math.round(item.stake_saturation * 100000) / 1000}%`} + + {!item.avg_uptime_percent ? '-' : `${item.avg_uptime_percent}%`} + + {!item.pending_events.length ? ( + (onItemActionClick ? onItemActionClick(item, action) : undefined)} + disableRedeemingRewards={!item.accumulated_rewards || item.accumulated_rewards.amount === '0'} + disableDelegateMore={(item?.stake_saturation || 0) > 100} + /> + ) : ( + + + + )} + + + )) + ) : ( + + + + {isLoading ? : You have not delegated to any mixnodes} + + + + )} + +
+
+ ); +}; diff --git a/nym-wallet/src/components/Delegation/DelegationModal.stories.tsx b/nym-wallet/src/components/Delegation/DelegationModal.stories.tsx new file mode 100644 index 0000000000..6e5c88df1a --- /dev/null +++ b/nym-wallet/src/components/Delegation/DelegationModal.stories.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { ComponentMeta } from '@storybook/react'; + +import { Paper } from '@mui/material'; +import { Delegations } from './Delegations'; +import { items } from './DelegationList.stories'; +import { DelegationModal } from './DelegationModal'; + +const explorerUrl = 'https://sandbox-explorer.nymtech.net'; + +export default { + title: 'Delegation/Components/Delegation Modals', + component: Delegations, +} as ComponentMeta; + +const transactionUrl = + 'https://sandbox-blocks.nymtech.net/transactions/11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CFB'; +const balance = '104 NYMT'; +const recipient = 'nymt1923pujepxfnv8dqyxqrl078s4ysf3xn2p7z2xa'; + +const Content: React.FC = () => ( + +

Your Delegations

+ +
+); + +export const Loading = () => ( + <> + + + +); + +export const DelegateSuccess = () => ( + <> + + + +); + +export const UndelegateSuccess = () => ( + <> + + + +); + +export const RedeemSuccess = () => ( + <> + + + +); + +export const RedeemAllSuccess = () => ( + <> + + + +); + +export const Error = () => ( + <> + + + +); diff --git a/nym-wallet/src/components/Delegation/DelegationModal.tsx b/nym-wallet/src/components/Delegation/DelegationModal.tsx new file mode 100644 index 0000000000..4a37c05250 --- /dev/null +++ b/nym-wallet/src/components/Delegation/DelegationModal.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { Box, Button, CircularProgress, Link, Modal, Stack, Typography } from '@mui/material'; +import { modalStyle } from '../Modals/styles'; +import { TPoolOption } from '../TokenPoolSelector'; + +export type ActionType = 'delegate' | 'undelegate' | 'redeem' | 'redeem-all' | 'compound'; + +const actionToHeader = (action: ActionType): string => { + // eslint-disable-next-line default-case + switch (action) { + case 'redeem': + return 'Rewards redeemed successfully'; + case 'redeem-all': + return 'All rewards redeemed successfully'; + case 'delegate': + return 'Delegation complete'; + case 'undelegate': + return 'Undelegation complete'; + case 'compound': + return 'Undelegation complete'; + } + return 'Oh no! Something went wrong!'; +}; + +export type DelegationModalProps = { + status: 'loading' | 'success' | 'error'; + action: ActionType; + message?: string; + recipient?: string; + balance?: string; + transactionUrl?: string; + tokenPool?: TPoolOption; +}; + +export const DelegationModal: React.FC< + DelegationModalProps & { + open: boolean; + onClose?: () => void; + } +> = ({ status, action, message, recipient, balance, transactionUrl, open, onClose, tokenPool, children }) => { + if (status === 'loading') { + return ( + + + + + Please wait... + + + + ); + } + + if (status === 'error') { + return ( + + + theme.palette.error.main} mb={1}> + Oh no! Something went wrong... + + {message} + {children} + + + + ); + } + return ( + + + theme.palette.success.main} mb={1}> + {actionToHeader(action)} + + {message} + + {recipient && ( + theme.palette.text.secondary}> + Recipient: {recipient} + + )} + theme.palette.text.secondary}> + Your current {tokenPool === 'locked' ? 'locked balance' : 'balance'}: {balance} + + theme.palette.text.secondary}> + Check the transaction hash{' '} + + here + + + {children} + + + + ); +}; diff --git a/nym-wallet/src/components/Delegation/Delegations.stories.tsx b/nym-wallet/src/components/Delegation/Delegations.stories.tsx new file mode 100644 index 0000000000..99a37271bb --- /dev/null +++ b/nym-wallet/src/components/Delegation/Delegations.stories.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { ComponentMeta } from '@storybook/react'; + +import { Paper } from '@mui/material'; +import { Delegations } from './Delegations'; +import { items } from './DelegationList.stories'; + +const explorerUrl = 'https://sandbox-explorer.nymtech.net'; + +export default { + title: 'Delegation/Components/Delegations', + component: Delegations, +} as ComponentMeta; + +export const Default = () => ( + +

Your Delegations

+ +
+); + +export const Empty = () => ( + +

Your Delegations

+ +
+); diff --git a/nym-wallet/src/components/Delegation/Delegations.tsx b/nym-wallet/src/components/Delegation/Delegations.tsx new file mode 100644 index 0000000000..9958e1b1be --- /dev/null +++ b/nym-wallet/src/components/Delegation/Delegations.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Box, Link, Typography } from '@mui/material'; +import { DelegationWithEverything } from '@nymproject/types'; +import { DelegationList } from './DelegationList'; +import { DelegationListItemActions } from './DelegationActions'; + +export const Delegations: React.FC<{ + isLoading?: boolean; + items?: DelegationWithEverything[]; + explorerUrl: string; + onDelegationItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void; +}> = ({ isLoading, items, explorerUrl, onDelegationItemActionClick }) => ( + <> + + + theme.palette.text.primary} + > + Check the{' '} + + list of mixnodes + {' '} + for uptime and performance to make delegation decisions + + + +); diff --git a/nym-wallet/src/components/Delegation/ModalListItem.tsx b/nym-wallet/src/components/Delegation/ModalListItem.tsx new file mode 100644 index 0000000000..24da1f7506 --- /dev/null +++ b/nym-wallet/src/components/Delegation/ModalListItem.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Box, Stack, Typography } from '@mui/material'; +import { ModalDivider } from '../Modals/ModalDivider'; + +export const ModalListItem: React.FC<{ + label: string; + divider?: boolean; + hidden?: boolean; + value: React.ReactNode; +}> = ({ label, value, hidden, divider }) => ( + +); diff --git a/nym-wallet/src/components/Delegation/Modals.stories.tsx b/nym-wallet/src/components/Delegation/Modals.stories.tsx new file mode 100644 index 0000000000..025013e818 --- /dev/null +++ b/nym-wallet/src/components/Delegation/Modals.stories.tsx @@ -0,0 +1,129 @@ +import React from 'react'; + +import { Button, Paper } from '@mui/material'; +import { DelegateModal } from './DelegateModal'; +import { UndelegateModal } from './UndelegateModal'; + +export default { + title: 'Delegation/Components/Action Modals', +}; + +const Background: React.FC<{ onOpen: () => void }> = ({ onOpen }) => ( + +

Lorem ipsum

+ +

+ Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis sunt + velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur culpa + exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa. +

+

+ Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eiusmod occaecat aliquip ad + tempor. Labore amet laborum magna amet consequat dolor cupidatat in consequat sunt aliquip magna laboris tempor + culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna elit ut + mollit. +

+

+ Labore voluptate elit amet ipsum qui officia duis in et occaecat culpa ex do non labore mollit. Cillum cupidatat + duis ea dolore laboris laboris sunt duis anim consectetur cupidatat nulla ad minim sunt ea. Aliqua amet commodo + est irure sint magna sunt. Pariatur dolore commodo labore quis incididunt proident duis voluptate exercitation in + duis. Occaecat aliqua laboris reprehenderit nostrud est aute pariatur fugiat anim. Dolore sunt cillum ea aliquip + consectetur laborum ipsum qui veniam Lorem consectetur adipisicing velit magna aute. Amet tempor quis excepteur + minim culpa velit Lorem enim ad. +

+

+ Mollit laborum exercitation excepteur laboris adipisicing ipsum veniam cillum mollit voluptate do. Amet et anim + Lorem mollit minim duis cupidatat non. Consectetur sit deserunt nisi nisi non excepteur dolor eiusmod aute aute + irure anim dolore ipsum et veniam. +

+
+); + +export const Delegate = () => { + const [open, setOpen] = React.useState(true); + return ( + <> + setOpen(true)} /> + setOpen(false)} + onOk={async () => setOpen(false)} + currency="NYM" + feeOverride="0.004375" + estimatedReward={50.423} + accountBalance="425.2345053" + nodeUptimePercentage={99.28394} + profitMarginPercentage={11.12334234} + rewardInterval="weekly" + hasVestingContract={false} + /> + + ); +}; + +export const DelegateBelowMinimum = () => { + const [open, setOpen] = React.useState(true); + return ( + <> + setOpen(true)} /> + setOpen(false)} + onOk={async () => setOpen(false)} + currency="NYM" + feeOverride="0.004375" + estimatedReward={425.2345053} + nodeUptimePercentage={99.28394} + profitMarginPercentage={11.12334234} + rewardInterval="weekly" + initialAmount="0.1" + hasVestingContract={false} + /> + + ); +}; + +export const DelegateMore = () => { + const [open, setOpen] = React.useState(true); + return ( + <> + setOpen(true)} /> + setOpen(false)} + onOk={async () => setOpen(false)} + header="Delegate more" + buttonText="Delegate more" + currency="NYM" + feeOverride="0.004375" + estimatedReward={50.423} + accountBalance="425.2345053" + nodeUptimePercentage={99.28394} + profitMarginPercentage={11.12334234} + rewardInterval="weekly" + hasVestingContract={false} + /> + + ); +}; + +export const Undelegate = () => { + const [open, setOpen] = React.useState(true); + return ( + <> + setOpen(true)} /> + setOpen(false)} + onOk={() => setOpen(false)} + currency="NYM" + fee={0.004375} + amount={150} + identityKey="AA6RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujyxx" + proxy={null} + /> + + ); +}; diff --git a/nym-wallet/src/components/Delegation/PendingEvents.tsx b/nym-wallet/src/components/Delegation/PendingEvents.tsx new file mode 100644 index 0000000000..d33c292774 --- /dev/null +++ b/nym-wallet/src/components/Delegation/PendingEvents.tsx @@ -0,0 +1,155 @@ +import React, { FC } from 'react'; +import { + Box, + Link, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TableSortLabel, + Tooltip, + Typography, +} from '@mui/material'; +import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; +import { DelegationEvent } from '@nymproject/types'; +import { ArrowDropDown } from '@mui/icons-material'; +import { visuallyHidden } from '@mui/utils'; + +type Order = 'asc' | 'desc'; + +interface HeadCell { + id: keyof DelegationEvent; + label: string; + sortable: boolean; + disablePadding?: boolean; +} + +interface EnhancedTableProps { + onRequestSort: (event: React.MouseEvent, property: keyof DelegationEvent) => void; + order: Order; + orderBy: string; +} + +const headCells: HeadCell[] = [ + { id: 'node_identity', label: 'Node ID', sortable: true }, + { id: 'amount', label: 'Delegation', sortable: true }, + { id: 'kind', label: 'Type', sortable: true }, +]; + +function descendingComparator(a: T, b: T, orderBy: keyof T) { + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; +} + +function getComparator( + order: Order, + orderBy: Key, +): (a: DelegationEvent, b: DelegationEvent) => number { + return order === 'desc' + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +} + +const EnhancedTableHead: React.FC = ({ order, orderBy, onRequestSort }) => { + const createSortHandler = (property: keyof DelegationEvent) => (event: React.MouseEvent) => { + onRequestSort(event, property); + }; + + return ( + + + {headCells.map((headCell) => ( + + + {headCell.label} + {orderBy === headCell.id ? ( + + {order === 'desc' ? 'sorted descending' : 'sorted ascending'} + + ) : null} + + + ))} + + + ); +}; + +export const PendingEvents: FC<{ pendingEvents: DelegationEvent[]; explorerUrl: string }> = ({ + pendingEvents, + explorerUrl, +}) => { + const [order, setOrder] = React.useState('asc'); + const [orderBy, setOrderBy] = React.useState('node_identity'); + + const handleRequestSort = (event: React.MouseEvent, property: keyof DelegationEvent) => { + const isAsc = orderBy === property && order === 'asc'; + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(property); + }; + + if (pendingEvents.length === 0) return No pending events; + + return ( + + + + + {pendingEvents.sort(getComparator(order, orderBy)).map((item, index) => ( + + + + Copy identity key {item.node_identity} to clipboard + + } + /> + + Click to view {item.node_identity} in the Network Explorer + + } + placement="right" + arrow + > + + {item.node_identity.slice(0, 6)}...{item.node_identity.slice(-6)} + + + + {!item.amount ? '-' : `${item.amount?.amount} ${item.amount?.denom}`} + {item.kind === 'Delegate' ? 'Delegation' : 'Undelegation'} + + ))} + +
+
+ ); +}; diff --git a/nym-wallet/src/components/Delegation/UndelegateModal.tsx b/nym-wallet/src/components/Delegation/UndelegateModal.tsx new file mode 100644 index 0000000000..350fb739a8 --- /dev/null +++ b/nym-wallet/src/components/Delegation/UndelegateModal.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Stack, Typography } from '@mui/material'; +import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; +import { SimpleModal } from '../Modals/SimpleModal'; + +export const UndelegateModal: React.FC<{ + open: boolean; + onClose?: () => void; + onOk?: (identityKey: string, proxy: string | null) => void; + identityKey: string; + amount: number; + fee: number; + currency: string; + proxy: string | null; +}> = ({ identityKey, open, onClose, onOk, amount, fee, currency, proxy }) => { + const handleOk = () => { + if (onOk) { + onOk(identityKey, proxy); + } + }; + return ( + + + + + Delegation amount: + + {amount} {currency} + + + + + Tokens will be transferred to account you are logged in with now + + + + theme.palette.nym.fee}> + Est. fee for this transaction: + + theme.palette.nym.fee}> + {fee} {currency} + + + + ); +}; diff --git a/nym-wallet/src/components/Delegation/types.ts b/nym-wallet/src/components/Delegation/types.ts new file mode 100644 index 0000000000..a9382745ed --- /dev/null +++ b/nym-wallet/src/components/Delegation/types.ts @@ -0,0 +1,23 @@ +export interface DelegateListItem { + /** Node identity key */ + id: string; + /** Date of delegation */ + delegationDate: Date; + /** Delegated amount as a string including the currency, e.g. 1.05 NYM */ + amount: string; // TODO: fix up + /** Reward amount as a string, e.g. 1.05 NYM on mainnet */ + reward?: string; + /** A number between 0 and 1 */ + profitMarginPercentage?: number; + /** A number between 0 and 1 */ + uptimePercentage?: number; + /** Is pending */ + isPending?: DelegateListItemPending; +} + +export interface DelegateListItemPending { + /** Either the user is delegating or undelegating */ + actionType: 'delegate' | 'undelegate'; + /** Pending transaction */ + blockHeight: number; +} diff --git a/nym-wallet/src/components/Fee.tsx b/nym-wallet/src/components/Fee.tsx index 4b2ae45646..950cbf9e20 100644 --- a/nym-wallet/src/components/Fee.tsx +++ b/nym-wallet/src/components/Fee.tsx @@ -1,12 +1,12 @@ import React, { useState, useEffect, useContext } from 'react'; import { Typography } from '@mui/material'; -import { Operation } from '../types'; +import { Operation } from '@nymproject/types'; import { getGasFee } from '../requests'; import { AppContext } from '../context/main'; export const Fee = ({ feeType }: { feeType: Operation }) => { const [fee, setFee] = useState(); - const { currency } = useContext(AppContext); + const { clientDetails } = useContext(AppContext); const getFee = async () => { const res = await getGasFee(feeType); @@ -20,7 +20,7 @@ export const Fee = ({ feeType }: { feeType: Operation }) => { if (fee) { return ( - Estimated fee for this transaction: {`${fee} ${currency?.major}`}{' '} + Est.fee for this transaction: {`${fee} ${clientDetails?.denom}`}{' '} ); } diff --git a/nym-wallet/src/components/LoadingPage.tsx b/nym-wallet/src/components/LoadingPage.tsx index 7143bf49a0..0d8969f690 100644 --- a/nym-wallet/src/components/LoadingPage.tsx +++ b/nym-wallet/src/components/LoadingPage.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Box, LinearProgress, Stack } from '@mui/material'; -import { NymWordmark } from '@nymproject/react'; +import { NymWordmark } from '@nymproject/react/logo/NymWordmark'; import { AuthTheme } from 'src/theme'; export const LoadingPage = () => ( diff --git a/nym-wallet/src/components/Modals/ModalDivider.tsx b/nym-wallet/src/components/Modals/ModalDivider.tsx new file mode 100644 index 0000000000..5bae3a8b50 --- /dev/null +++ b/nym-wallet/src/components/Modals/ModalDivider.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { Box, SxProps } from '@mui/material'; + +export const ModalDivider: React.FC<{ + sx?: SxProps; +}> = ({ sx }) => ; diff --git a/nym-wallet/src/components/Modals/SimpleModal.stories.tsx b/nym-wallet/src/components/Modals/SimpleModal.stories.tsx new file mode 100644 index 0000000000..7cb71929ed --- /dev/null +++ b/nym-wallet/src/components/Modals/SimpleModal.stories.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { ComponentMeta } from '@storybook/react'; + +import { Button, Paper } from '@mui/material'; +import { SimpleModal } from './SimpleModal'; +import { ModalDivider } from './ModalDivider'; + +export default { + title: 'Modals/Simple Modal', + component: SimpleModal, +} as ComponentMeta; + +export const Default = () => { + const [open, setOpen] = React.useState(true); + return ( + <> + +

Lorem ipsum

+ +

+ Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis + sunt velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur + culpa exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa. +

+

+ Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eiusmod occaecat aliquip + ad tempor. Labore amet laborum magna amet consequat dolor cupidatat in consequat sunt aliquip magna laboris + tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna + elit ut mollit. +

+

+ Labore voluptate elit amet ipsum qui officia duis in et occaecat culpa ex do non labore mollit. Cillum + cupidatat duis ea dolore laboris laboris sunt duis anim consectetur cupidatat nulla ad minim sunt ea. Aliqua + amet commodo est irure sint magna sunt. Pariatur dolore commodo labore quis incididunt proident duis voluptate + exercitation in duis. Occaecat aliqua laboris reprehenderit nostrud est aute pariatur fugiat anim. Dolore sunt + cillum ea aliquip consectetur laborum ipsum qui veniam Lorem consectetur adipisicing velit magna aute. Amet + tempor quis excepteur minim culpa velit Lorem enim ad. +

+

+ Mollit laborum exercitation excepteur laboris adipisicing ipsum veniam cillum mollit voluptate do. Amet et + anim Lorem mollit minim duis cupidatat non. Consectetur sit deserunt nisi nisi non excepteur dolor eiusmod + aute aute irure anim dolore ipsum et veniam. +

+
+ setOpen(false)} + onOk={() => setOpen(false)} + header="This is a modal" + subHeader="This is a sub header" + okLabel="Click to continue" + > +

Lorem mollit minim duis cupidatat non. Consectetur sit deserunt

+

+ Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis. +

+ +

Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eius

+

+ Tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna + elit ut mollit. +

+
+ + ); +}; + +export const NoSubheader = () => { + const [open, setOpen] = React.useState(true); + return ( + <> + +

Lorem ipsum

+ +

+ Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis + sunt velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur + culpa exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa. +

+

+ Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eiusmod occaecat aliquip + ad tempor. Labore amet laborum magna amet consequat dolor cupidatat in consequat sunt aliquip magna laboris + tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna + elit ut mollit. +

+

+ Labore voluptate elit amet ipsum qui officia duis in et occaecat culpa ex do non labore mollit. Cillum + cupidatat duis ea dolore laboris laboris sunt duis anim consectetur cupidatat nulla ad minim sunt ea. Aliqua + amet commodo est irure sint magna sunt. Pariatur dolore commodo labore quis incididunt proident duis voluptate + exercitation in duis. Occaecat aliqua laboris reprehenderit nostrud est aute pariatur fugiat anim. Dolore sunt + cillum ea aliquip consectetur laborum ipsum qui veniam Lorem consectetur adipisicing velit magna aute. Amet + tempor quis excepteur minim culpa velit Lorem enim ad. +

+

+ Mollit laborum exercitation excepteur laboris adipisicing ipsum veniam cillum mollit voluptate do. Amet et + anim Lorem mollit minim duis cupidatat non. Consectetur sit deserunt nisi nisi non excepteur dolor eiusmod + aute aute irure anim dolore ipsum et veniam. +

+
+ setOpen(false)} + onOk={() => setOpen(false)} + header="This is a modal" + okLabel="Kaplow!" + > +

+ Tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna + elit ut mollit. +

+ +

+ Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis. +

+
+ + ); +}; diff --git a/nym-wallet/src/components/Modals/SimpleModal.tsx b/nym-wallet/src/components/Modals/SimpleModal.tsx new file mode 100644 index 0000000000..53e7c79d5a --- /dev/null +++ b/nym-wallet/src/components/Modals/SimpleModal.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { modalStyle } from './styles'; + +export const SimpleModal: React.FC<{ + open: boolean; + onClose?: () => void; + onOk?: () => void; + header: string; + subHeader?: string; + okLabel: string; + okDisabled?: boolean; + sx?: SxProps; +}> = ({ open, onClose, okDisabled, onOk, header, subHeader, okLabel, sx, children }) => ( + + + + + {header} + + + + {subHeader && ( + theme.palette.text.secondary}> + {subHeader} + + )} + + {children} + + + + +); diff --git a/nym-wallet/src/components/Modals/styles.ts b/nym-wallet/src/components/Modals/styles.ts new file mode 100644 index 0000000000..de4621a2a8 --- /dev/null +++ b/nym-wallet/src/components/Modals/styles.ts @@ -0,0 +1,11 @@ +export const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 500, + bgcolor: 'background.paper', + boxShadow: 24, + borderRadius: '16px', + p: 4, +}; diff --git a/nym-wallet/src/components/Nav.tsx b/nym-wallet/src/components/Nav.tsx index 91d3cea8a8..6cc07ab359 100644 --- a/nym-wallet/src/components/Nav.tsx +++ b/nym-wallet/src/components/Nav.tsx @@ -1,9 +1,9 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useContext } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material'; import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material'; import { AppContext } from '../context/main'; -import { Bond, Delegate, Unbond, Undelegate } from '../svg-icons'; +import { Bond, Delegate, Unbond } from '../svg-icons'; const routesSchema = [ { @@ -32,30 +32,27 @@ const routesSchema = [ Icon: Unbond, }, { - label: 'Delegate', - route: '/delegate', + label: 'Delegation', + route: '/delegation', Icon: Delegate, }, { - label: 'Undelegate', - route: '/undelegate', - Icon: Undelegate, + label: 'Docs', + route: '/docs', + Icon: Description, + mode: 'dev', + }, + { + label: 'Admin', + route: '/admin', + Icon: Settings, + mode: 'admin', }, ]; export const Nav = () => { - const { isAdminAddress, handleShowAdmin } = useContext(AppContext); const location = useLocation(); - - useEffect(() => { - if (isAdminAddress) { - routesSchema.push({ - label: 'Docs', - route: '/docs', - Icon: Description, - }); - } - }, [isAdminAddress]); + const { isAdminAddress } = useContext(AppContext); return (
{ }} > - {routesSchema.map(({ Icon, route, label }) => ( - - - - - - - ))} - {isAdminAddress && ( - - - - - - - )} + {routesSchema + .filter(({ mode }) => { + if (!mode) { + return true; + } + switch (mode) { + case 'admin': + return isAdminAddress; + case 'dev': + return isAdminAddress; + default: + return false; + } + }) + .map(({ Icon, route, label }) => ( + + + + + + + ))}
); diff --git a/nym-wallet/src/components/NetworkSelector.tsx b/nym-wallet/src/components/NetworkSelector.tsx index 7febfbc435..f5d7ee762e 100644 --- a/nym-wallet/src/components/NetworkSelector.tsx +++ b/nym-wallet/src/components/NetworkSelector.tsx @@ -1,9 +1,9 @@ import React, { useState, useContext } from 'react'; import { Button, List, ListItem, ListItemIcon, ListItemText, ListSubheader, Popover } from '@mui/material'; import { ArrowDropDown, CheckSharp } from '@mui/icons-material'; +import { Network } from 'src/types'; import { AppContext } from '../context/main'; -import { config } from '../../config'; -import { Network } from '../types'; +import { config } from '../config'; const networks: { networkName: Network; name: string }[] = [ { networkName: 'MAINNET', name: 'Nym Mainnet' }, diff --git a/nym-wallet/src/components/NodeStatus.tsx b/nym-wallet/src/components/NodeStatus.tsx index 8b58492eab..eec37b8113 100644 --- a/nym-wallet/src/components/NodeStatus.tsx +++ b/nym-wallet/src/components/NodeStatus.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Typography } from '@mui/material'; import { CircleOutlined, PauseCircleOutlined, CheckCircleOutline } from '@mui/icons-material'; -import { MixnodeStatus } from '../types'; +import { MixnodeStatus } from '@nymproject/types'; const Active = () => ( diff --git a/nym-wallet/src/components/NodeTypeSelector.tsx b/nym-wallet/src/components/NodeTypeSelector.tsx index 707b25b6e3..eb29d40e49 100644 --- a/nym-wallet/src/components/NodeTypeSelector.tsx +++ b/nym-wallet/src/components/NodeTypeSelector.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material'; -import { EnumNodeType } from '../types/global'; +import { EnumNodeType } from '@nymproject/types'; export const NodeTypeSelector = ({ disabled, diff --git a/nym-wallet/src/components/NymLogo.tsx b/nym-wallet/src/components/NymLogo.tsx index 4d407642fe..735fff23af 100644 --- a/nym-wallet/src/components/NymLogo.tsx +++ b/nym-wallet/src/components/NymLogo.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { NymLogo as NymLogoReact } from '@nymproject/react'; +import { NymLogo as NymLogoReact } from '@nymproject/react/logo/NymLogo'; const imgSize = { small: 40, diff --git a/nym-wallet/src/components/Rewards/CompoundModal.tsx b/nym-wallet/src/components/Rewards/CompoundModal.tsx new file mode 100644 index 0000000000..d8d23dfc7a --- /dev/null +++ b/nym-wallet/src/components/Rewards/CompoundModal.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { Alert, AlertTitle, Stack, Typography } from '@mui/material'; +import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; +import WarningIcon from '@mui/icons-material/Warning'; +import { SimpleModal } from '../Modals/SimpleModal'; + +export const CompoundModal: React.FC<{ + open: boolean; + onClose?: () => void; + onOk?: (identityKey: string) => void; + identityKey: string; + amount: number; + fee: number; + minimum?: number; + currency: string; + message: string; +}> = ({ open, onClose, onOk, identityKey, amount, fee, currency, message }) => { + const handleOk = () => { + if (onOk) { + onOk(identityKey); + } + }; + return ( + + {identityKey && } + + + Rewards amount: + + {amount} {currency} + + + + + Rewards will be transferred to account you are logged in with now + + + + theme.palette.nym.fee}> + Est. fee for this transaction: + + theme.palette.nym.fee}> + {fee} {currency} + + + + {amount < fee && ( + }> + Warning: fees are greater than the reward + The fees for redeeming rewards will cost more than the rewards. Are you sure you want to continue? + + )} + + ); +}; diff --git a/nym-wallet/src/components/Rewards/RedeemModal.stories.tsx b/nym-wallet/src/components/Rewards/RedeemModal.stories.tsx new file mode 100644 index 0000000000..f483397c35 --- /dev/null +++ b/nym-wallet/src/components/Rewards/RedeemModal.stories.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { ComponentMeta } from '@storybook/react'; + +import { Button, Paper } from '@mui/material'; +import { RedeemModal } from './RedeemModal'; + +export default { + title: 'Rewards/Components/Redeem Modals', + component: RedeemModal, +} as ComponentMeta; + +const Content: React.FC<{ + setOpen: (value: boolean) => void; +}> = ({ setOpen }) => ( + +

Lorem ipsum

+ +

+ Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis sunt + velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur culpa + exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa. +

+

+ Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eiusmod occaecat aliquip ad + tempor. Labore amet laborum magna amet consequat dolor cupidatat in consequat sunt aliquip magna laboris tempor + culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna elit ut + mollit. +

+

+ Labore voluptate elit amet ipsum qui officia duis in et occaecat culpa ex do non labore mollit. Cillum cupidatat + duis ea dolore laboris laboris sunt duis anim consectetur cupidatat nulla ad minim sunt ea. Aliqua amet commodo + est irure sint magna sunt. Pariatur dolore commodo labore quis incididunt proident duis voluptate exercitation in + duis. Occaecat aliqua laboris reprehenderit nostrud est aute pariatur fugiat anim. Dolore sunt cillum ea aliquip + consectetur laborum ipsum qui veniam Lorem consectetur adipisicing velit magna aute. Amet tempor quis excepteur + minim culpa velit Lorem enim ad. +

+

+ Mollit laborum exercitation excepteur laboris adipisicing ipsum veniam cillum mollit voluptate do. Amet et anim + Lorem mollit minim duis cupidatat non. Consectetur sit deserunt nisi nisi non excepteur dolor eiusmod aute aute + irure anim dolore ipsum et veniam. +

+
+); + +export const RedeemAllRewards = () => { + const [open, setOpen] = React.useState(true); + return ( + <> + + setOpen(false)} + onOk={() => setOpen(false)} + message="Redeem all rewards" + currency="NYM" + identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa" + fee={0.004375} + amount={425.65843} + /> + + ); +}; + +export const RedeemRewardForMixnode = () => { + const [open, setOpen] = React.useState(true); + return ( + <> + + setOpen(false)} + onOk={() => setOpen(false)} + message="Redeem rewards" + currency="NYM" + identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa" + fee={0.004375} + amount={425.65843} + /> + + ); +}; + +export const FeeIsMoreThanAllRewards = () => { + const [open, setOpen] = React.useState(true); + return ( + <> + + setOpen(false)} + onOk={() => setOpen(false)} + message="Redeem all rewards" + currency="NYM" + identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa" + fee={0.004375} + amount={0.001} + /> + + ); +}; + +export const FeeIsMoreThanMixnodeReward = () => { + const [open, setOpen] = React.useState(true); + return ( + <> + + setOpen(false)} + onOk={() => setOpen(false)} + identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa" + message="Redeem rewards" + currency="NYM" + fee={0.004375} + amount={0.001} + /> + + ); +}; diff --git a/nym-wallet/src/components/Rewards/RedeemModal.tsx b/nym-wallet/src/components/Rewards/RedeemModal.tsx new file mode 100644 index 0000000000..dc12068f2b --- /dev/null +++ b/nym-wallet/src/components/Rewards/RedeemModal.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { Alert, AlertTitle, Stack, Typography } from '@mui/material'; +import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; +import WarningIcon from '@mui/icons-material/Warning'; +import { SimpleModal } from '../Modals/SimpleModal'; + +export const RedeemModal: React.FC<{ + open: boolean; + onClose?: () => void; + onOk?: (identityKey: string) => void; + identityKey: string; + amount: number; + fee: number; + minimum?: number; + currency: string; + message: string; +}> = ({ open, onClose, onOk, identityKey, amount, fee, currency, message }) => { + const handleOk = () => { + if (onOk) { + onOk(identityKey); + } + }; + return ( + + {identityKey && } + + + Rewards amount: + + {amount} {currency} + + + + + Rewards will be transferred to account you are logged in with now + + + + theme.palette.nym.fee}> + Est. fee for this transaction: + + theme.palette.nym.fee}> + {fee} {currency} + + + + {amount < fee && ( + }> + Warning: fees are greater than the reward + The fees for redeeming rewards will cost more than the rewards. Are you sure you want to continue? + + )} + + ); +}; diff --git a/nym-wallet/src/components/Rewards/RewardsSummary.stories.tsx b/nym-wallet/src/components/Rewards/RewardsSummary.stories.tsx new file mode 100644 index 0000000000..d824eb33d6 --- /dev/null +++ b/nym-wallet/src/components/Rewards/RewardsSummary.stories.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { ComponentMeta } from '@storybook/react'; + +import { Paper } from '@mui/material'; +import { RewardsSummary } from './RewardsSummary'; + +export default { + title: 'Rewards/Components/Rewards Summary', + component: RewardsSummary, +} as ComponentMeta; + +export const Default = () => ( + + + +); + +export const Empty = () => ( + + + +); + +export const Loading = () => ( + + + +); diff --git a/nym-wallet/src/components/Rewards/RewardsSummary.tsx b/nym-wallet/src/components/Rewards/RewardsSummary.tsx new file mode 100644 index 0000000000..f81b3297c8 --- /dev/null +++ b/nym-wallet/src/components/Rewards/RewardsSummary.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button, CircularProgress, Stack, Tooltip, Typography } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; + +export const RewardsSummary: React.FC<{ + isLoading?: boolean; + totalDelegation?: string; + totalRewards?: string; + onClickRedeemAll?: () => void; +}> = ({ isLoading, totalDelegation, totalRewards, onClickRedeemAll }) => { + const theme = useTheme(); + return ( + + + + Total delegations: + + {isLoading ? : totalDelegation || '-'} + + + + New rewards: + + {isLoading ? : totalRewards || '-'} + + + + + + {/* */} + + + + ); +}; diff --git a/nym-wallet/src/components/TokenPoolSelector.tsx b/nym-wallet/src/components/TokenPoolSelector.tsx index 99da52f9dc..2879f95d87 100644 --- a/nym-wallet/src/components/TokenPoolSelector.tsx +++ b/nym-wallet/src/components/TokenPoolSelector.tsx @@ -2,7 +2,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { FormControl, InputLabel, ListItemText, MenuItem, Select, SelectChangeEvent, Typography } from '@mui/material'; import { AppContext } from '../context/main'; -type TPoolOption = 'balance' | 'locked'; +export type TPoolOption = 'balance' | 'locked'; export const TokenPoolSelector: React.FC<{ disabled: boolean; onSelect: (pool: TPoolOption) => void }> = ({ disabled, @@ -11,7 +11,7 @@ export const TokenPoolSelector: React.FC<{ disabled: boolean; onSelect: (pool: T const [value, setValue] = useState('balance'); const { userBalance: { tokenAllocation, balance, fetchBalance, fetchTokenAllocation }, - currency, + clientDetails, } = useContext(AppContext); useEffect(() => { @@ -48,7 +48,7 @@ export const TokenPoolSelector: React.FC<{ disabled: boolean; onSelect: (pool: T {tokenAllocation && ( )} diff --git a/nym-wallet/config.ts b/nym-wallet/src/config.ts similarity index 56% rename from nym-wallet/config.ts rename to nym-wallet/src/config.ts index 6c6eaff9b9..1b1f83ce1f 100644 --- a/nym-wallet/config.ts +++ b/nym-wallet/src/config.ts @@ -1,3 +1,4 @@ export const config = { IS_DEV_MODE: process.env.NODE_ENV === 'development', + LOG_TAURI_OPERATIONS: process.env.NODE_ENV === 'development', }; diff --git a/nym-wallet/src/context/accounts.tsx b/nym-wallet/src/context/accounts.tsx index 8445ce2be2..9725a9570c 100644 --- a/nym-wallet/src/context/accounts.tsx +++ b/nym-wallet/src/context/accounts.tsx @@ -1,5 +1,5 @@ import React, { createContext, Dispatch, SetStateAction, useContext, useEffect, useMemo, useState } from 'react'; -import { AccountEntry } from 'src/types'; +import { AccountEntry } from '@nymproject/types'; import { addAccount as addAccountRequest, showMnemonicForAccount } from 'src/requests'; import { useSnackbar } from 'notistack'; import { AppContext } from './main'; diff --git a/nym-wallet/src/context/delegations.tsx b/nym-wallet/src/context/delegations.tsx new file mode 100644 index 0000000000..b19b617476 --- /dev/null +++ b/nym-wallet/src/context/delegations.tsx @@ -0,0 +1,175 @@ +import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { getDelegationSummary, undelegateFromMixnode } from 'src/requests/delegation'; +import { + DelegationEvent, + DelegationWithEverything, + MajorCurrencyAmount, + TransactionExecuteResult, +} from '@nymproject/types'; +import type { Network } from 'src/types'; +import { + claimDelegatorRewards, + compoundDelegatorRewards, + delegateToMixnode, + getAllPendingDelegations, + vestingClaimDelegatorRewards, + vestingCompoundDelegatorRewards, + vestingDelegateToMixnode, + vestingUndelegateFromMixnode, +} from 'src/requests'; +import { TPoolOption } from 'src/components'; + +export type TDelegationContext = { + isLoading: boolean; + error?: string; + delegations?: DelegationWithEverything[]; + pendingDelegations?: DelegationEvent[]; + totalDelegations?: string; + totalRewards?: string; + refresh: () => Promise; + addDelegation: ( + data: { identity: string; amount: MajorCurrencyAmount }, + tokenPool: TPoolOption, + ) => Promise; + undelegate: (identity: string, proxy: string | null) => Promise; + redeemRewards: (identity: string, proxy: string | null) => Promise; + compoundRewards: (identity: string, proxy: string | null) => Promise; +}; + +export type TDelegationTransaction = { + transactionUrl: string; +}; + +export const DelegationContext = createContext({ + isLoading: true, + refresh: async () => undefined, + addDelegation: async () => { + throw new Error('Not implemented'); + }, + undelegate: async () => { + throw new Error('Not implemented'); + }, + redeemRewards: async () => { + throw new Error('Not implemented'); + }, + compoundRewards: async () => { + throw new Error('Not implemented'); + }, +}); + +export const DelegationContextProvider: FC<{ + network?: Network; +}> = ({ network, children }) => { + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(); + const [delegations, setDelegations] = useState(); + const [totalDelegations, setTotalDelegations] = useState(); + const [totalRewards, setTotalRewards] = useState(); + const [pendingDelegations, setPendingDelegations] = useState(); + + const addDelegation = async (data: { identity: string; amount: MajorCurrencyAmount }, tokenPool: TPoolOption) => { + try { + let tx; + + if (tokenPool === 'locked') tx = await vestingDelegateToMixnode(data); + else tx = await delegateToMixnode(data); + + return tx; + } catch (e) { + throw new Error(e as string); + } + }; + + const undelegate = async (identity: string, proxy: string | null) => { + let delegationResult; + try { + if ((proxy || '').trim().length === 0) { + // the owner of the delegation is main account (the owner of the vesting account), so it is delegation with unlocked tokens + delegationResult = await undelegateFromMixnode(identity); + } else { + // the delegation is with locked tokens, so use the vesting contract + delegationResult = await vestingUndelegateFromMixnode(identity); + } + return delegationResult; + } catch (e) { + throw new Error(e as string); + } + }; + + const redeemRewards = async (identity: string, proxy: string | null) => { + try { + if ((proxy || '').trim().length === 0) { + // the owner of the delegation is main account (the owner of the vesting account), so it is delegation with unlocked tokens + await claimDelegatorRewards(identity); + } else { + // the delegation is with locked tokens, so use the vesting contract + await vestingClaimDelegatorRewards(identity); + } + } catch (e) { + throw new Error(e as string); + } + }; + + const compoundRewards = async (identity: string, proxy: string | null) => { + try { + if ((proxy || '').trim().length === 0) { + // the owner of the delegation is main account (the owner of the vesting account), so it is delegation with unlocked tokens + await compoundDelegatorRewards(identity); + } else { + // the delegation is with locked tokens, so use the vesting contract + await vestingCompoundDelegatorRewards(identity); + } + } catch (e) { + throw new Error(e as string); + } + }; + + const resetState = () => { + setIsLoading(true); + setError(undefined); + setTotalDelegations(undefined); + setTotalRewards(undefined); + setDelegations([]); + }; + + const refresh = useCallback(async () => { + try { + const data = await getDelegationSummary(); + const pending = await getAllPendingDelegations(); + + setPendingDelegations(pending); + setDelegations(data.delegations); + setTotalDelegations(`${data.total_delegations.amount} ${data.total_delegations.denom}`); + setTotalRewards(`${data.total_rewards.amount} ${data.total_rewards.denom}`); + } catch (e) { + setError((e as Error).message); + } + setIsLoading(false); + }, [network]); + + useEffect(() => { + resetState(); + refresh(); + }, [network]); + + const memoizedValue = useMemo( + () => ({ + isLoading, + error, + delegations, + pendingDelegations, + totalDelegations, + totalRewards, + refresh, + addDelegation, + undelegate, + redeemRewards, + compoundRewards, + }), + [isLoading, error, delegations, pendingDelegations, totalDelegations], + ); + + return {children}; +}; + +export const useDelegationContext = () => useContext(DelegationContext); diff --git a/nym-wallet/src/context/main.tsx b/nym-wallet/src/context/main.tsx index 1f97606d89..bfeb82b64b 100644 --- a/nym-wallet/src/context/main.tsx +++ b/nym-wallet/src/context/main.tsx @@ -1,19 +1,20 @@ -import React, { useMemo, createContext, useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; +import React, { createContext, useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useSnackbar } from 'notistack'; -import { Account, Network, TCurrency, TMixnodeBondDetails, AccountEntry, AppEnv } from '../types'; +import { Account, AccountEntry, MixNodeBond } from '@nymproject/types'; +import { getVersion } from '@tauri-apps/api/app'; +import { AppEnv, Network } from '../types'; import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance'; import { + getEnv, getMixnodeBondDetails, + listAccounts, selectNetwork, signInWithMnemonic, signInWithPassword, signOut, switchAccount, - getEnv, - listAccounts, } from '../requests'; -import { currencyMap } from '../utils'; import { Console } from '../utils/console'; export const urls = (networkName?: Network) => @@ -32,15 +33,14 @@ type TLoginType = 'mnemonic' | 'password'; type TAppContext = { mode: 'light' | 'dark'; appEnv?: AppEnv; + appVersion?: string; clientDetails?: Account; storedAccounts?: AccountEntry[]; - mixnodeDetails?: TMixnodeBondDetails | null; + mixnodeDetails?: MixNodeBond | null; userBalance: TUseuserBalance; showAdmin: boolean; - showSettings: boolean; showTerminal: boolean; network?: Network; - currency?: TCurrency; isLoading: boolean; isAdminAddress: boolean; error?: string; @@ -49,7 +49,6 @@ type TAppContext = { setError: (value?: string) => void; switchNetwork: (network: Network) => void; getBondDetails: () => Promise; - handleShowSettings: () => void; handleShowAdmin: () => void; logIn: (opts: { type: TLoginType; value: string }) => void; handleShowTerminal: () => void; @@ -63,20 +62,20 @@ export const AppContext = createContext({} as TAppContext); export const AppProvider = ({ children }: { children: React.ReactNode }) => { const [clientDetails, setClientDetails] = useState(); const [storedAccounts, setStoredAccounts] = useState(); - const [mixnodeDetails, setMixnodeDetails] = useState(); + const [mixnodeDetails, setMixnodeDetails] = useState(null); const [network, setNetwork] = useState(); const [appEnv, setAppEnv] = useState(); - const [currency, setCurrency] = useState(); const [showAdmin, setShowAdmin] = useState(false); - const [showSettings, setShowSettings] = useState(false); const [showTerminal, setShowTerminal] = useState(false); const [mode] = useState<'light' | 'dark'>('light'); const [loginType, setLoginType] = useState<'mnemonic' | 'password'>(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); + const [appVersion, setAppVersion] = useState(); + const [isAdminAddress, setIsAdminAddress] = useState(false); - const userBalance = useGetBalance(clientDetails?.client_address); - const history = useHistory(); + const userBalance = useGetBalance(clientDetails); + const navigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); const clearState = () => { @@ -85,7 +84,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { setNetwork(undefined); setError(undefined); setIsLoading(false); - setMixnodeDetails(undefined); + setMixnodeDetails(null); }; const loadAccount = async (n: Network) => { @@ -95,8 +94,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { } catch (e) { enqueueSnackbar('Error loading account', { variant: 'error' }); Console.error(e as string); - } finally { - setCurrency(currencyMap(n)); } }; @@ -106,7 +103,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }; const getBondDetails = async () => { - setMixnodeDetails(undefined); + setMixnodeDetails(null); try { const mixnode = await getMixnodeBondDetails(); setMixnodeDetails(mixnode); @@ -122,10 +119,14 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { } }; + useEffect(() => { + getVersion().then(setAppVersion); + }, []); + useEffect(() => { if (!clientDetails) { clearState(); - history.push('/'); + navigate('/'); } }, [clientDetails]); @@ -136,6 +137,18 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { } }, [network]); + useEffect(() => { + let newValue = false; + if (network && appEnv?.ADMIN_ADDRESS && clientDetails?.client_address) { + const adminAddressMap = JSON.parse(appEnv.ADMIN_ADDRESS); + const adminAddresses = adminAddressMap[network] || []; + if (adminAddresses.length) { + newValue = adminAddresses.includes(clientDetails?.client_address); + } + } + setIsAdminAddress(newValue); + }, [appEnv, network]); + const logIn = async ({ type, value }: { type: TLoginType; value: string }) => { if (value.length === 0) { setError(`A ${type} must be provided`); @@ -151,7 +164,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { setLoginType('password'); } setNetwork('MAINNET'); - history.push('/balance'); + navigate('/balance'); } catch (e) { setError(e as string); } finally { @@ -181,7 +194,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }; const handleShowAdmin = () => setShowAdmin((show) => !show); - const handleShowSettings = () => setShowSettings((show) => !show); const handleShowTerminal = () => setShowTerminal((show) => !show); const switchNetwork = (_network: Network) => setNetwork(_network); @@ -189,7 +201,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { () => ({ mode, appEnv, - isAdminAddress: Boolean(appEnv?.ADMIN_ADDRESS && clientDetails?.client_address === appEnv.ADMIN_ADDRESS), + appVersion, + isAdminAddress, isLoading, error, clientDetails, @@ -197,17 +210,14 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { mixnodeDetails, userBalance, showAdmin, - showSettings, showTerminal, network, - currency, loginType, setIsLoading, setError, signInWithPassword, switchNetwork, getBondDetails, - handleShowSettings, handleShowAdmin, handleShowTerminal, logIn, @@ -215,7 +225,9 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { onAccountChange, }), [ + appVersion, loginType, + isAdminAddress, mode, appEnv, isLoading, @@ -224,9 +236,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { mixnodeDetails, userBalance, showAdmin, - showSettings, network, - currency, storedAccounts, showTerminal, ], diff --git a/nym-wallet/src/context/mocks/accounts.tsx b/nym-wallet/src/context/mocks/accounts.tsx index 4374f812cc..7a3ff838e1 100644 --- a/nym-wallet/src/context/mocks/accounts.tsx +++ b/nym-wallet/src/context/mocks/accounts.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from 'react'; -import { AccountEntry } from 'src/types'; +import { AccountEntry } from '@nymproject/types'; import { AccountsContext, TAccountMnemonic, TAccountsDialog } from '../accounts'; export const MockAccountsProvider: React.FC = ({ children }) => { diff --git a/nym-wallet/src/context/mocks/delegations.tsx b/nym-wallet/src/context/mocks/delegations.tsx new file mode 100644 index 0000000000..4a159ee643 --- /dev/null +++ b/nym-wallet/src/context/mocks/delegations.tsx @@ -0,0 +1,227 @@ +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { TransactionExecuteResult, DelegationWithEverything, MajorCurrencyAmount } from '@nymproject/types'; +import { DelegationContext, TDelegationTransaction } from '../delegations'; + +import { mockSleep } from './utils'; + +const SLEEP_MS = 1000; + +let mockDelegations: DelegationWithEverything[] = [ + { + node_identity: 'FiojKW7oY9WQmLCiYAsCA21tpowZHS6zcUoyYm319p6Z', + delegated_on_iso_datetime: new Date(2021, 1, 1).toDateString(), + accumulated_rewards: { amount: '0.05', denom: 'NYM' }, + amount: { amount: '10', denom: 'NYM' }, + profit_margin_percent: 0.1122323949234, + owner: '', + block_height: BigInt(100), + stake_saturation: 0.5, + proxy: '', + avg_uptime_percent: 0.5, + total_delegation: { amount: '0', denom: 'NYM' }, + pledge_amount: { amount: '0', denom: 'NYM' }, + pending_events: [], + history: [], + }, + { + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + accumulated_rewards: { amount: '0.1', denom: 'NYM' }, + amount: { amount: '100', denom: 'NYM' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + profit_margin_percent: 0.89, + owner: '', + block_height: BigInt(4000), + stake_saturation: 0.5, + proxy: '', + avg_uptime_percent: 0.1, + total_delegation: { amount: '0', denom: 'NYM' }, + pledge_amount: { amount: '0', denom: 'NYM' }, + pending_events: [], + history: [], + }, +]; + +export const MockDelegationContextProvider: FC<{}> = ({ children }) => { + const [trigger, setTrigger] = useState(new Date()); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(); + const [delegations, setDelegations] = useState(); + const [totalDelegations, setTotalDelegations] = useState(); + + const triggerStateUpdate = () => setTrigger(new Date()); + + const getDelegations = async (): Promise => + mockDelegations.sort((a, b) => a.node_identity.localeCompare(b.node_identity)); + + const recalculate = async () => { + const newDelegations = await getDelegations(); + const newTotalDelegations = `${newDelegations.length * 100} NYM`; + setDelegations(newDelegations); + setTotalDelegations(newTotalDelegations); + }; + + const addDelegation = async ({ + identity, + amount, + }: { + identity: string; + amount: MajorCurrencyAmount; + }): Promise => { + await mockSleep(SLEEP_MS); + // mockDelegations.push({ ...newDelegation }); + await recalculate(); + triggerStateUpdate(); + + setTimeout(async () => { + mockDelegations = mockDelegations.map((d) => { + if (d.node_identity === identity) { + return { ...d, isPending: undefined }; + } + return d; + }); + await recalculate(); + triggerStateUpdate(); + }, 3000); + + return { + logs_json: '', + data_json: '', + gas_info: { + gas_wanted: BigInt(1), + gas_used: BigInt(1), + fee: { amount: '1', denom: 'NYM' }, + }, + transaction_hash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354', + fee: { amount: '1', denom: 'NYM' }, + }; + }; + + const updateDelegation = async ( + newDelegation: DelegationWithEverything, + ignorePendingForStorybook?: boolean, + ): Promise => { + if (ignorePendingForStorybook) { + mockDelegations = mockDelegations.map((d) => { + if (d.node_identity === newDelegation.node_identity) { + return { ...newDelegation }; + } + return d; + }); + await recalculate(); + triggerStateUpdate(); + return { + transactionUrl: + 'https://sandbox-blocks.nymtech.net/transactions/55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354', + }; + } + + await mockSleep(SLEEP_MS); + mockDelegations = mockDelegations.map((d) => { + if (d.node_identity === newDelegation.node_identity) { + return { ...newDelegation, isPending: { blockHeight: 1234, actionType: 'delegate' } }; + } + return d; + }); + await recalculate(); + triggerStateUpdate(); + + setTimeout(async () => { + mockDelegations = mockDelegations.map((d) => { + if (d.node_identity === newDelegation.node_identity) { + return { ...d, isPending: undefined }; + } + return d; + }); + await recalculate(); + triggerStateUpdate(); + }, 3000); + + return { + transactionUrl: + 'https://sandbox-blocks.nymtech.net/transactions/55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354', + }; + }; + + const undelegate = async (mixnodeAddress: string): Promise => { + await mockSleep(SLEEP_MS); + mockDelegations = mockDelegations.map((d) => { + if (d.node_identity === mixnodeAddress) { + return { ...d, isPending: { blockHeight: 5678, actionType: 'undelegate' } }; + } + return d; + }); + await recalculate(); + triggerStateUpdate(); + + setTimeout(async () => { + mockDelegations = mockDelegations.filter((d) => d.node_identity !== mixnodeAddress); + await recalculate(); + triggerStateUpdate(); + }, 3000); + + return { + logs_json: '', + data_json: '', + transaction_hash: '', + gas_info: { + gas_wanted: BigInt(1), + gas_used: BigInt(1), + fee: { amount: '1', denom: 'NYM' }, + }, + fee: { amount: '1', denom: 'NYM' }, + }; + }; + + const redeemRewards = async () => { + throw new Error('Not implemented'); + }; + + const compoundRewards = async () => { + throw new Error('Not implemented'); + }; + + const resetState = () => { + setIsLoading(true); + setError(undefined); + setTotalDelegations(undefined); + setDelegations([]); + }; + + const refresh = useCallback(async () => { + resetState(); + setTimeout(async () => { + try { + await mockSleep(SLEEP_MS); + await recalculate(); + } catch (e) { + setError((e as Error).message); + } + setIsLoading(false); + }, 2000); + }, []); + + useEffect(() => { + // reset state and refresh + resetState(); + refresh(); + }, []); + + const memoizedValue = useMemo( + () => ({ + isLoading, + error, + delegations, + totalDelegations, + refresh, + getDelegations, + addDelegation, + updateDelegation, + undelegate, + redeemRewards, + compoundRewards, + }), + [isLoading, error, delegations, totalDelegations, trigger], + ); + + return {children}; +}; diff --git a/nym-wallet/src/context/mocks/rewards.tsx b/nym-wallet/src/context/mocks/rewards.tsx new file mode 100644 index 0000000000..1202fde78f --- /dev/null +++ b/nym-wallet/src/context/mocks/rewards.tsx @@ -0,0 +1,90 @@ +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { RewardsContext, TRewardsTransaction } from '../rewards'; +import { useDelegationContext } from '../delegations'; +import { mockSleep } from './utils'; + +export const MockRewardsContextProvider: FC = ({ children }) => { + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(); + const [totalRewards, setTotalRewards] = useState(); + const { delegations } = useDelegationContext(); + const delegationsHash = delegations?.map((d) => d.accumulated_rewards).join(','); + + const resetState = () => { + setIsLoading(true); + setError(undefined); + setTotalRewards(undefined); + }; + + const recalculate = () => { + const sum: number | undefined = delegations + ?.map((d) => (d.accumulated_rewards ? Number(10) : Number(0))) + .reduce((acc, cur) => acc + cur, Number(0)); + + setTotalRewards(sum ? `${sum} NYM` : undefined); + }; + + const refresh = useCallback(async () => { + resetState(); + setTimeout(() => { + recalculate(); + setIsLoading(false); + }, 1500); + }, [delegationsHash]); + + useEffect(() => { + recalculate(); + }, [delegationsHash]); + + useEffect(() => { + // reset state and refresh + resetState(); + refresh(); + }, []); + + const redeemRewards = async (mixnodeAddress: string): Promise => { + if (!delegations) { + throw new Error('No delegations'); + } + + const d = delegations.find((d1) => d1.node_identity === mixnodeAddress); + + if (!d) { + throw new Error(`Unable to find delegation for id = ${mixnodeAddress}`); + } + + await mockSleep(1000); + + return { + transactionUrl: + 'https://sandbox-blocks.nymtech.net/transactions/55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354', + }; + }; + + const redeemAllRewards = async (): Promise => { + if (!delegations) { + throw new Error('No delegations'); + } + + await mockSleep(1000); + + return { + transactionUrl: + 'https://sandbox-blocks.nymtech.net/transactions/55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354', + }; + }; + + const memoizedValue = useMemo( + () => ({ + isLoading, + error, + totalRewards, + refresh, + redeemRewards, + redeemAllRewards, + }), + [isLoading, error, totalRewards], + ); + + return {children}; +}; diff --git a/nym-wallet/src/context/mocks/utils.ts b/nym-wallet/src/context/mocks/utils.ts new file mode 100644 index 0000000000..f4b13fff36 --- /dev/null +++ b/nym-wallet/src/context/mocks/utils.ts @@ -0,0 +1,3 @@ +export const mockSleep = (delayMilliseconds: number) => + // eslint-disable-next-line no-promise-executor-return + new Promise((resolve) => setTimeout(resolve, delayMilliseconds)); diff --git a/nym-wallet/src/context/rewards.tsx b/nym-wallet/src/context/rewards.tsx new file mode 100644 index 0000000000..63e86dd38a --- /dev/null +++ b/nym-wallet/src/context/rewards.tsx @@ -0,0 +1,67 @@ +import React, { createContext, FC, useContext, useEffect, useMemo, useState } from 'react'; +import { Network } from 'src/types'; +import { useDelegationContext } from './delegations'; + +type TRewardsContext = { + isLoading: boolean; + error?: string; + totalRewards?: string; + refresh: () => Promise; + redeemRewards: (mixnodeAddress: string) => Promise; + redeemAllRewards: () => Promise; +}; + +export type TRewardsTransaction = { + transactionUrl: string; +}; + +export const RewardsContext = createContext({ + isLoading: true, + refresh: async () => undefined, + redeemRewards: async () => { + throw new Error('Not implemented'); + }, + redeemAllRewards: async () => { + throw new Error('Not implemented'); + }, +}); + +export const RewardsContextProvider: FC<{ + network?: Network; +}> = ({ network, children }) => { + const { isLoading, totalRewards, refresh } = useDelegationContext(); + const [currentNetwork, setCurrentNetwork] = useState(); + const [error, setError] = useState(); + + const resetState = async () => { + setError(undefined); + }; + + useEffect(() => { + if (currentNetwork !== network) { + // reset state and refresh + resetState(); + setCurrentNetwork(network); + } + }, [network]); + + const memoizedValue = useMemo( + () => ({ + isLoading, + error, + totalRewards, + refresh, + redeemRewards: async () => { + throw new Error('Not implemented'); + }, + redeemAllRewards: async () => { + throw new Error('Not implemented'); + }, + }), + [isLoading, error, totalRewards, network], + ); + + return {children}; +}; + +export const useRewardsContext = () => useContext(RewardsContext); diff --git a/nym-wallet/src/hooks/useGetBalance.ts b/nym-wallet/src/hooks/useGetBalance.ts index 9056f1d4d2..3c8015fe8f 100644 --- a/nym-wallet/src/hooks/useGetBalance.ts +++ b/nym-wallet/src/hooks/useGetBalance.ts @@ -1,7 +1,13 @@ import { useCallback, useEffect, useState } from 'react'; import { invoke } from '@tauri-apps/api'; -import { VestingAccountInfo } from 'src/types/rust/vestingaccountinfo'; -import { Balance, Coin, OriginalVestingResponse, Period } from '../types'; +import { + Account, + Balance, + MajorCurrencyAmount, + OriginalVestingResponse, + Period, + VestingAccountInfo, +} from '@nymproject/types'; import { getVestingCoins, getVestedCoins, @@ -14,7 +20,7 @@ import { import { Console } from '../utils/console'; type TTokenAllocation = { - [key in 'vesting' | 'vested' | 'locked' | 'spendable']: Coin['amount']; + [key in 'vesting' | 'vested' | 'locked' | 'spendable']: MajorCurrencyAmount['amount']; }; export type TUseuserBalance = { @@ -25,13 +31,13 @@ export type TUseuserBalance = { currentVestingPeriod?: Period; vestingAccountInfo?: VestingAccountInfo; isLoading: boolean; - fetchBalance: () => void; + fetchBalance: () => Promise; + fetchTokenAllocation: () => Promise; clearBalance: () => void; clearAll: () => void; - fetchTokenAllocation: () => void; }; -export const useGetBalance = (address?: string): TUseuserBalance => { +export const useGetBalance = (clientDetails?: Account): TUseuserBalance => { const [balance, setBalance] = useState(); const [error, setError] = useState(); const [tokenAllocation, setTokenAllocation] = useState(); @@ -46,7 +52,7 @@ export const useGetBalance = (address?: string): TUseuserBalance => { const fetchTokenAllocation = async () => { setIsLoading(true); - if (address) { + if (clientDetails?.client_address) { try { const [ originalVestingValue, @@ -54,19 +60,19 @@ export const useGetBalance = (address?: string): TUseuserBalance => { vestedCoins, lockedCoins, spendableCoins, - currentVestingPer, + currentPeriod, vestingAccountDetail, ] = await Promise.all([ - getOriginalVesting(address), - getVestingCoins(address), - getVestedCoins(address), + getOriginalVesting(clientDetails?.client_address), + getVestingCoins(clientDetails?.client_address), + getVestedCoins(clientDetails?.client_address), getLockedCoins(), getSpendableCoins(), - getCurrentVestingPeriod(address), - getVestingAccountInfo(address), + getCurrentVestingPeriod(clientDetails?.client_address), + getVestingAccountInfo(clientDetails?.client_address), ]); setOriginalVesting(originalVestingValue); - setCurrentVestingPeriod(currentVestingPer); + setCurrentVestingPeriod(currentPeriod); setTokenAllocation({ vesting: vestingCoins.amount, vested: vestedCoins.amount, @@ -112,8 +118,8 @@ export const useGetBalance = (address?: string): TUseuserBalance => { }; useEffect(() => { - handleRefresh(address); - }, [address]); + handleRefresh(clientDetails?.client_address); + }, [clientDetails]); return { error, diff --git a/nym-wallet/src/index.tsx b/nym-wallet/src/index.tsx index 76d20f0ec0..b585c8c3d3 100644 --- a/nym-wallet/src/index.tsx +++ b/nym-wallet/src/index.tsx @@ -8,10 +8,14 @@ import { AppProvider } from './context/main'; import { ErrorFallback } from './components'; import { NymWalletTheme } from './theme'; import { maximizeWindow } from './utils'; +import { config } from './config'; const App = () => { useEffect(() => { - maximizeWindow(); + // do not maximise in dev mode, because it happens on hot reloading + if (!config.IS_DEV_MODE) { + maximizeWindow(); + } }, []); return ( diff --git a/nym-wallet/src/layouts/AppLayout.tsx b/nym-wallet/src/layouts/AppLayout.tsx index c583b3be5e..2cb54855c6 100644 --- a/nym-wallet/src/layouts/AppLayout.tsx +++ b/nym-wallet/src/layouts/AppLayout.tsx @@ -1,19 +1,17 @@ import React, { useContext } from 'react'; -import { NymWordmark } from '@nymproject/react'; +import { NymWordmark } from '@nymproject/react/logo/NymWordmark'; import { Box, Container } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { AppContext } from 'src/context'; -import { Settings } from 'src/pages'; import { AppBar, LoadingPage, Nav } from '../components'; export const ApplicationLayout: React.FC = ({ children }) => { const theme = useTheme(); - const { isLoading, showSettings } = useContext(AppContext); + const { isLoading, appVersion } = useContext(AppContext); return ( <> {isLoading && } - {showSettings && } {