in-wallet verification of mix bonding signature

This commit is contained in:
Jędrzej Stuczyński
2023-02-23 17:19:13 +00:00
parent a86cac5b85
commit 2f87481348
15 changed files with 909 additions and 118 deletions
@@ -43,6 +43,7 @@ pub use cosmrs::tendermint::Time as TendermintTime;
pub use cosmrs::tx::{self, Gas};
pub use cosmrs::Coin as CosmosCoin;
pub use cosmrs::{bip32, AccountId, Decimal, Denom};
use cosmwasm_std::Addr;
pub use cosmwasm_std::Coin as CosmWasmCoin;
pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
pub use signing_client::Client as SigningNyxdClient;
@@ -369,6 +370,15 @@ where
&self.client_address.as_ref().unwrap()[0]
}
pub fn cw_address(&self) -> Addr
where
C: SigningCosmWasmClient,
{
// the call to unchecked is fine here as we're converting directly from `AccountId`
// which must have been a valid bech32 address
Addr::unchecked(self.address().as_ref())
}
pub fn signer(&self) -> &DirectSecp256k1HdWallet
where
C: SigningCosmWasmClient,
@@ -6,6 +6,7 @@ use crate::nyxd::error::NyxdError;
use crate::nyxd::NyxdClient;
use async_trait::async_trait;
use cosmrs::AccountId;
use nym_contracts_common::signing::Nonce;
use nym_mixnet_contract_common::delegation::{MixNodeDelegationResponse, OwnerProxySubKey};
use nym_mixnet_contract_common::families::Family;
use nym_mixnet_contract_common::mixnode::{
@@ -390,6 +391,13 @@ pub trait MixnetQueryClient {
.await
}
async fn get_signing_nonce(&self, address: &AccountId) -> Result<Nonce, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetSigningNonce {
address: address.to_string(),
})
.await
}
async fn get_node_family_by_label(&self, label: &str) -> Result<Option<Family>, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetFamilyByLabel {
label: label.to_string(),
@@ -3,14 +3,90 @@
use cosmwasm_std::{from_slice, to_vec, Addr, Coin, MessageInfo, StdResult};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::{Display, Formatter};
pub type Nonce = u32;
// define this type explicitly for [hopefully] better usability
// (so you wouldn't need to worry about whether you should use bytes, bs58, etc.)
#[derive(Clone)]
pub struct MessageSignature(Vec<u8>);
impl MessageSignature {
pub fn as_bs58_string(&self) -> String {
bs58::encode(&self.0).into_string()
}
}
impl<'a> From<&'a [u8]> for MessageSignature {
fn from(value: &'a [u8]) -> Self {
MessageSignature(value.to_vec())
}
}
impl From<Vec<u8>> for MessageSignature {
fn from(value: Vec<u8>) -> Self {
MessageSignature(value)
}
}
impl TryFrom<String> for MessageSignature {
type Error = bs58::decode::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(MessageSignature(bs58::decode(value).into_vec()?))
}
}
impl AsRef<[u8]> for MessageSignature {
#[inline]
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl<'de> Deserialize<'de> for MessageSignature {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let inner = String::deserialize(deserializer)?;
let bytes = bs58::decode(inner).into_vec().map_err(de::Error::custom)?;
Ok(MessageSignature(bytes))
}
}
impl Serialize for MessageSignature {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let bs58_encoded = self.as_bs58_string();
bs58_encoded.serialize(serializer)
}
}
impl Display for MessageSignature {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_bs58_string())
}
}
pub trait SigningPurpose {
fn message_type() -> MessageType;
}
#[derive(Serialize)]
#[serde(transparent)]
pub struct MessageType(String);
impl MessageType {
pub fn new<S: Into<String>>(typ: S) -> Self {
MessageType(typ.into())
}
}
impl<T> From<T> for MessageType
where
T: ToString,
@@ -20,7 +96,7 @@ where
}
}
#[derive(Default, Serialize, Deserialize)]
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum SigningAlgorithm {
#[default]
@@ -28,12 +104,20 @@ pub enum SigningAlgorithm {
Secp256k1,
}
impl SigningAlgorithm {
pub fn is_ed25519(&self) -> bool {
matches!(self, SigningAlgorithm::Ed25519)
}
}
// TODO: maybe move this one to repo-wide common?
// TODO: should it perhaps also include the public key itself?
#[derive(Serialize, Deserialize)]
pub struct SignableMessage<T> {
nonce: u32,
algorithm: SigningAlgorithm,
content: T,
pub nonce: u32,
pub algorithm: SigningAlgorithm,
pub content: T,
}
impl<T> SignableMessage<T> {
@@ -83,24 +167,21 @@ impl<T> SignableMessage<T> {
}
#[derive(Serialize)]
struct ContractMessageContent<T> {
message_type: MessageType,
signer: Addr,
proxy: Option<Addr>,
funds: Vec<Coin>,
data: T,
pub struct ContractMessageContent<T> {
pub message_type: MessageType,
pub signer: Addr,
pub proxy: Option<Addr>,
pub funds: Vec<Coin>,
pub data: T,
}
impl<T> ContractMessageContent<T> {
pub fn new(
message_type: MessageType,
signer: Addr,
proxy: Option<Addr>,
funds: Vec<Coin>,
data: T,
) -> Self {
impl<T> ContractMessageContent<T>
where
T: SigningPurpose,
{
pub fn new(signer: Addr, proxy: Option<Addr>, funds: Vec<Coin>, data: T) -> Self {
ContractMessageContent {
message_type,
message_type: T::message_type(),
signer,
proxy,
funds,
@@ -108,12 +189,7 @@ impl<T> ContractMessageContent<T> {
}
}
pub fn new_with_info(
message_type: MessageType,
info: MessageInfo,
signer: Addr,
data: T,
) -> Self {
pub fn new_with_info(info: MessageInfo, signer: Addr, data: T) -> Self {
let proxy = if info.sender == signer {
None
} else {
@@ -121,7 +197,7 @@ impl<T> ContractMessageContent<T> {
};
ContractMessageContent {
message_type,
message_type: T::message_type(),
signer,
proxy,
funds: info.funds,
@@ -17,6 +17,7 @@ mod msg;
pub mod pending_events;
pub mod reward_params;
pub mod rewarding;
pub mod signing_types;
mod types;
pub use contracts_common::types::*;
@@ -43,4 +44,5 @@ pub use pending_events::{
PendingIntervalEventData, PendingIntervalEventKind,
};
pub use reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate, RewardingParams};
pub use signing_types::*;
pub use types::*;
@@ -10,7 +10,6 @@ use crate::reward_params::{
};
use crate::{delegation, ContractStateParams, Layer, LayerAssignment, MixId, Percent};
use crate::{Gateway, IdentityKey, MixNode};
use contracts_common::signing::MessageType;
use cosmwasm_std::Decimal;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -241,54 +240,6 @@ pub enum ExecuteMsg {
}
impl ExecuteMsg {
pub fn message_type(&self) -> MessageType {
match self {
ExecuteMsg::AssignNodeLayer { .. } => {}
ExecuteMsg::CreateFamily { .. } => {}
ExecuteMsg::JoinFamily { .. } => {}
ExecuteMsg::LeaveFamily { .. } => {}
ExecuteMsg::KickFamilyMember { .. } => {}
ExecuteMsg::CreateFamilyOnBehalf { .. } => {}
ExecuteMsg::JoinFamilyOnBehalf { .. } => {}
ExecuteMsg::LeaveFamilyOnBehalf { .. } => {}
ExecuteMsg::KickFamilyMemberOnBehalf { .. } => {}
ExecuteMsg::UpdateRewardingValidatorAddress { .. } => {}
ExecuteMsg::UpdateContractStateParams { .. } => {}
ExecuteMsg::UpdateActiveSetSize { .. } => {}
ExecuteMsg::UpdateRewardingParams { .. } => {}
ExecuteMsg::UpdateIntervalConfig { .. } => {}
ExecuteMsg::BeginEpochTransition => {}
ExecuteMsg::AdvanceCurrentEpoch { .. } => {}
ExecuteMsg::ReconcileEpochEvents { .. } => {}
ExecuteMsg::BondMixnode { .. } => {}
ExecuteMsg::BondMixnodeOnBehalf { .. } => {}
ExecuteMsg::PledgeMore { .. } => {}
ExecuteMsg::PledgeMoreOnBehalf { .. } => {}
ExecuteMsg::UnbondMixnode { .. } => {}
ExecuteMsg::UnbondMixnodeOnBehalf { .. } => {}
ExecuteMsg::UpdateMixnodeCostParams { .. } => {}
ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { .. } => {}
ExecuteMsg::UpdateMixnodeConfig { .. } => {}
ExecuteMsg::UpdateMixnodeConfigOnBehalf { .. } => {}
ExecuteMsg::BondGateway { .. } => {}
ExecuteMsg::BondGatewayOnBehalf { .. } => {}
ExecuteMsg::UnbondGateway { .. } => {}
ExecuteMsg::UnbondGatewayOnBehalf { .. } => {}
ExecuteMsg::DelegateToMixnode { .. } => {}
ExecuteMsg::DelegateToMixnodeOnBehalf { .. } => {}
ExecuteMsg::UndelegateFromMixnode { .. } => {}
ExecuteMsg::UndelegateFromMixnodeOnBehalf { .. } => {}
ExecuteMsg::RewardMixnode { .. } => {}
ExecuteMsg::WithdrawOperatorReward { .. } => {}
ExecuteMsg::WithdrawOperatorRewardOnBehalf { .. } => {}
ExecuteMsg::WithdrawDelegatorReward { .. } => {}
ExecuteMsg::WithdrawDelegatorRewardOnBehalf { .. } => {}
#[cfg(feature = "contract-testing")]
ExecuteMsg::TestingResolveAllPendingEvents { .. } => {}
}
todo!()
}
pub fn default_memo(&self) -> String {
match self {
ExecuteMsg::AssignNodeLayer { mix_id, layer } => {
@@ -549,6 +500,11 @@ pub enum QueryMsg {
start_after: Option<u32>,
},
GetNumberOfPendingEvents {},
// signing-related
GetSigningNonce {
address: String,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
@@ -0,0 +1,118 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{Gateway, MixNode, MixNodeCostParams};
use contracts_common::signing::{MessageType, SigningPurpose};
use serde::Serialize;
#[derive(Serialize)]
pub struct MixnodeBondingPayload {
mix_node: MixNode,
cost_params: MixNodeCostParams,
}
impl MixnodeBondingPayload {
pub fn new(mix_node: MixNode, cost_params: MixNodeCostParams) -> Self {
Self {
mix_node,
cost_params,
}
}
}
impl SigningPurpose for MixnodeBondingPayload {
fn message_type() -> MessageType {
MessageType::new("mixnode-bonding")
}
}
#[derive(Serialize)]
pub struct GatewayBondingPayload {
gateway: Gateway,
}
impl GatewayBondingPayload {
pub fn new(gateway: Gateway) -> Self {
Self { gateway }
}
}
impl SigningPurpose for GatewayBondingPayload {
fn message_type() -> MessageType {
MessageType::new("gateway-bonding")
}
}
#[derive(Serialize)]
pub struct FamilyCreationSignature {
label: String,
// TODO: add any extra fields?
}
impl FamilyCreationSignature {
pub fn new(label: String) -> Self {
Self { label }
}
}
impl SigningPurpose for FamilyCreationSignature {
fn message_type() -> MessageType {
MessageType::new("family-creation")
}
}
#[derive(Serialize)]
pub struct FamilyJoinSignature {
family_head: String,
// TODO: add any extra fields?
}
impl FamilyJoinSignature {
pub fn new(family_head: String) -> Self {
Self { family_head }
}
}
impl SigningPurpose for FamilyJoinSignature {
fn message_type() -> MessageType {
MessageType::new("family-join")
}
}
#[derive(Serialize)]
pub struct FamilyLeaveSignature {
family_head: String,
// TODO: add any extra fields?
}
impl FamilyLeaveSignature {
pub fn new(family_head: String) -> Self {
Self { family_head }
}
}
impl SigningPurpose for FamilyLeaveSignature {
fn message_type() -> MessageType {
MessageType::new("family-leave")
}
}
#[derive(Serialize)]
pub struct FamilyKickSignature {
member: String,
// TODO: add any extra fields?
}
impl FamilyKickSignature {
pub fn new(member: String) -> Self {
Self { member }
}
}
impl SigningPurpose for FamilyKickSignature {
fn message_type() -> MessageType {
MessageType::new("family-member-removal")
}
}
// TODO: depending on our threat model, we should perhaps extend it to include all _on_behalf methods
+3
View File
@@ -581,6 +581,9 @@ pub fn query(
QueryMsg::GetNumberOfPendingEvents {} => to_binary(
&crate::interval::queries::query_number_of_pending_events(deps)?,
),
QueryMsg::GetSigningNonce { address } => to_binary(
&crate::signing::queries::query_current_signing_nonce(deps, address)?,
),
};
Ok(query_res?)
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::signing::storage::get_signing_nonce;
use cosmwasm_std::{Deps, StdResult};
use nym_contracts_common::signing::Nonce;
pub fn try_get_current_signing_nonce(deps: Deps<'_>, address: String) -> StdResult<Nonce> {
pub fn query_current_signing_nonce(deps: Deps<'_>, address: String) -> StdResult<Nonce> {
let address = deps.api.addr_validate(&address)?;
get_signing_nonce(deps.storage, address)
}
+1 -1
View File
@@ -16,7 +16,7 @@ use validator_client::nyxd;
#[derive(Args, Clone)]
#[clap(group(ArgGroup::new("sign").required(true).args(&["wallet_address", "text", "contract_msg"])))]
pub(crate) struct Sign {
pub struct Sign {
/// The id of the mixnode you want to sign with
#[clap(long)]
id: String,
+220 -36
View File
@@ -20,7 +20,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877"
dependencies = [
"generic-array",
"generic-array 0.14.6",
]
[[package]]
@@ -32,7 +32,8 @@ dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"opaque-debug",
"ctr",
"opaque-debug 0.3.0",
]
[[package]]
@@ -46,7 +47,7 @@ dependencies = [
"cipher",
"ctr",
"ghash",
"subtle",
"subtle 2.4.1",
]
[[package]]
@@ -97,7 +98,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25df3c03f1040d0069fcd3907e24e36d59f9b6fa07ba49be0eb25a794f036ba7"
dependencies = [
"base64ct",
"blake2",
"blake2 0.10.6",
"password-hash 0.3.2",
]
@@ -108,10 +109,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27e27b63e4a34caee411ade944981136fdfa535522dc9944d6700196cbd899f"
dependencies = [
"base64ct",
"blake2",
"blake2 0.10.6",
"password-hash 0.4.2",
]
[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
[[package]]
name = "async-trait"
version = "0.1.64"
@@ -227,7 +234,7 @@ dependencies = [
"rand_core 0.6.4",
"ripemd160",
"sha2 0.9.9",
"subtle",
"subtle 2.4.1",
"zeroize",
]
@@ -268,6 +275,18 @@ dependencies = [
"wyz",
]
[[package]]
name = "blake2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330"
dependencies = [
"byte-tools",
"crypto-mac 0.7.0",
"digest 0.8.1",
"opaque-debug 0.2.3",
]
[[package]]
name = "blake2"
version = "0.10.6"
@@ -290,7 +309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"block-padding",
"generic-array",
"generic-array 0.14.6",
]
[[package]]
@@ -299,7 +318,7 @@ version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
dependencies = [
"generic-array",
"generic-array 0.14.6",
]
[[package]]
@@ -318,7 +337,7 @@ dependencies = [
"group",
"pairing",
"rand_core 0.6.4",
"subtle",
"subtle 2.4.1",
"zeroize",
]
@@ -368,6 +387,12 @@ version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]]
name = "byte-tools"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
[[package]]
name = "bytemuck"
version = "1.13.0"
@@ -469,13 +494,23 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chacha"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862"
dependencies = [
"byteorder",
"keystream",
]
[[package]]
name = "cipher"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
dependencies = [
"generic-array",
"generic-array 0.14.6",
]
[[package]]
@@ -785,9 +820,9 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21"
dependencies = [
"generic-array",
"generic-array 0.14.6",
"rand_core 0.6.4",
"subtle",
"subtle 2.4.1",
"zeroize",
]
@@ -797,18 +832,28 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"generic-array 0.14.6",
"typenum",
]
[[package]]
name = "crypto-mac"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5"
dependencies = [
"generic-array 0.12.4",
"subtle 1.0.0",
]
[[package]]
name = "crypto-mac"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
dependencies = [
"generic-array",
"subtle",
"generic-array 0.14.6",
"subtle 2.4.1",
]
[[package]]
@@ -881,7 +926,7 @@ dependencies = [
"byteorder",
"digest 0.9.0",
"rand_core 0.5.1",
"subtle",
"subtle 2.4.1",
"zeroize",
]
@@ -1017,13 +1062,22 @@ dependencies = [
"syn",
]
[[package]]
name = "digest"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
dependencies = [
"generic-array 0.12.4",
]
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array",
"generic-array 0.14.6",
]
[[package]]
@@ -1034,7 +1088,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
dependencies = [
"block-buffer 0.10.3",
"crypto-common",
"subtle",
"subtle 2.4.1",
]
[[package]]
@@ -1146,6 +1200,8 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d"
dependencies = [
"curve25519-dalek",
"ed25519",
"rand 0.7.3",
"serde",
"sha2 0.9.9",
"zeroize",
]
@@ -1181,11 +1237,11 @@ dependencies = [
"crypto-bigint",
"der",
"ff",
"generic-array",
"generic-array 0.14.6",
"group",
"rand_core 0.6.4",
"sec1",
"subtle",
"subtle 2.4.1",
"zeroize",
]
@@ -1294,7 +1350,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924"
dependencies = [
"rand_core 0.6.4",
"subtle",
"subtle 2.4.1",
]
[[package]]
@@ -1580,6 +1636,15 @@ dependencies = [
"windows 0.39.0",
]
[[package]]
name = "generic-array"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
dependencies = [
"typenum",
]
[[package]]
name = "generic-array"
version = "0.14.6"
@@ -1597,8 +1662,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -1632,7 +1699,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99"
dependencies = [
"opaque-debug",
"opaque-debug 0.3.0",
"polyval",
]
@@ -1763,7 +1830,7 @@ dependencies = [
"byteorder",
"ff",
"rand_core 0.6.4",
"subtle",
"subtle 2.4.1",
]
[[package]]
@@ -1939,13 +2006,23 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
[[package]]
name = "hkdf"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b"
dependencies = [
"digest 0.9.0",
"hmac",
]
[[package]]
name = "hmac"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
dependencies = [
"crypto-mac",
"crypto-mac 0.11.1",
"digest 0.9.0",
]
@@ -2338,6 +2415,12 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "keystream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28"
[[package]]
name = "kuchiki"
version = "0.8.1"
@@ -2374,6 +2457,12 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "libm"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
[[package]]
name = "libz-sys"
version = "1.1.8"
@@ -2401,6 +2490,18 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]]
name = "lioness"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9"
dependencies = [
"arrayref",
"blake2 0.8.1",
"chacha",
"keystream",
]
[[package]]
name = "lock_api"
version = "0.4.9"
@@ -2650,6 +2751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg 1.1.0",
"libm",
]
[[package]]
@@ -2797,6 +2899,20 @@ dependencies = [
"zeroize",
]
[[package]]
name = "nym-crypto"
version = "0.1.0"
dependencies = [
"bs58",
"ed25519-dalek",
"nym-pemstore",
"nym-sphinx-types",
"rand 0.7.3",
"subtle-encoding",
"thiserror",
"x25519-dalek",
]
[[package]]
name = "nym-execute"
version = "0.1.0"
@@ -2866,6 +2982,13 @@ dependencies = [
"pem",
]
[[package]]
name = "nym-sphinx-types"
version = "0.1.0"
dependencies = [
"sphinx-packet",
]
[[package]]
name = "nym-types"
version = "1.0.0"
@@ -2961,6 +3084,7 @@ version = "1.1.11"
dependencies = [
"aes-gcm",
"argon2 0.3.4",
"async-trait",
"base64 0.13.1",
"bip39",
"cfg-if",
@@ -2977,6 +3101,8 @@ dependencies = [
"log",
"nym-coconut-interface",
"nym-config",
"nym-contracts-common",
"nym-crypto",
"nym-mixnet-contract-common",
"nym-types",
"nym-vesting-contract",
@@ -2985,6 +3111,7 @@ dependencies = [
"once_cell",
"pretty_env_logger",
"rand 0.6.5",
"rand_chacha 0.2.2",
"reqwest",
"serde",
"serde_json",
@@ -3071,6 +3198,12 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "opaque-debug"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
[[package]]
name = "opaque-debug"
version = "0.3.0"
@@ -3209,7 +3342,7 @@ checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
"subtle 2.4.1",
]
[[package]]
@@ -3220,7 +3353,7 @@ checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
"subtle 2.4.1",
]
[[package]]
@@ -3241,7 +3374,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739"
dependencies = [
"crypto-mac",
"crypto-mac 0.11.1",
]
[[package]]
@@ -3513,7 +3646,7 @@ checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"opaque-debug 0.3.0",
"universal-hash",
]
@@ -3764,6 +3897,16 @@ dependencies = [
"getrandom 0.2.8",
]
[[package]]
name = "rand_distr"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9532ada3929fb8b2e9dbe28d1e06c9b2cc65813f074fcb6bd5fbefeff9d56"
dependencies = [
"num-traits",
"rand 0.7.3",
]
[[package]]
name = "rand_hc"
version = "0.1.0"
@@ -4012,7 +4155,7 @@ checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251"
dependencies = [
"block-buffer 0.9.0",
"digest 0.9.0",
"opaque-debug",
"opaque-debug 0.3.0",
]
[[package]]
@@ -4162,9 +4305,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1"
dependencies = [
"der",
"generic-array",
"generic-array 0.14.6",
"pkcs8",
"subtle",
"subtle 2.4.1",
"zeroize",
]
@@ -4396,7 +4539,7 @@ dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.9.0",
"opaque-debug",
"opaque-debug 0.3.0",
]
[[package]]
@@ -4419,7 +4562,7 @@ dependencies = [
"block-buffer 0.9.0",
"digest 0.9.0",
"keccak",
"opaque-debug",
"opaque-debug 0.3.0",
]
[[package]]
@@ -4518,6 +4661,30 @@ dependencies = [
"system-deps 5.0.0",
]
[[package]]
name = "sphinx-packet"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc43eda802856ee82a7555c7b75ceb9e07451741c7a2f5f23d036020e01189d4"
dependencies = [
"aes",
"arrayref",
"blake2 0.8.1",
"bs58",
"byteorder",
"chacha",
"curve25519-dalek",
"digest 0.9.0",
"hkdf",
"hmac",
"lioness",
"log",
"rand 0.7.3",
"rand_distr",
"sha2 0.9.9",
"subtle 2.4.1",
]
[[package]]
name = "spin"
version = "0.5.2"
@@ -4609,6 +4776,12 @@ dependencies = [
"syn",
]
[[package]]
name = "subtle"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
[[package]]
name = "subtle"
version = "2.4.1"
@@ -4951,7 +5124,7 @@ dependencies = [
"serde_repr",
"sha2 0.9.9",
"signature",
"subtle",
"subtle 2.4.1",
"subtle-encoding",
"tendermint-proto",
"time",
@@ -5369,8 +5542,8 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05"
dependencies = [
"generic-array",
"subtle",
"generic-array 0.14.6",
"subtle 2.4.1",
]
[[package]]
@@ -6029,6 +6202,17 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "x25519-dalek"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f"
dependencies = [
"curve25519-dalek",
"rand_core 0.5.1",
"zeroize",
]
[[package]]
name = "xattr"
version = "0.2.3"
+5
View File
@@ -19,6 +19,7 @@ tauri-codegen = "=1.2.1"
tauri-macros = "=1.2.1"
[dependencies]
async-trait = "0.1.64"
bip39 = "1.0"
cfg-if = "1.0.0"
colored = "2.0"
@@ -57,6 +58,8 @@ cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegr
validator-client = { path = "../../common/client-libs/validator-client", features = [
"nyxd-client",
] }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" }
nym-vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" }
nym-config = { path = "../../common/config" }
@@ -67,6 +70,8 @@ nym-types = { path = "../../common/types" }
nym-wallet-types = { path = "../nym-wallet-types" }
[dev-dependencies]
nym-crypto = { path = "../../common/crypto", features = ["rand"] }
rand_chacha = "0.2"
tempfile = "3.3.0"
ts-rs = "6.1.2"
+13
View File
@@ -1,3 +1,6 @@
use nym_contracts_common::signing::SigningAlgorithm;
use nym_crypto::asymmetric::identity;
use nym_crypto::asymmetric::identity::{Ed25519RecoveryError, SignatureError};
use nym_types::error::TypesError;
use nym_wallet_types::network::Network;
use serde::{Serialize, Serializer};
@@ -122,6 +125,16 @@ pub enum BackendError {
#[error("Unable to open a new window")]
NewWindowError,
#[error("received unexpected signing algorithm: {received:?}. Expected to get {expected:?}")]
UnexpectedSigningAlgorithm {
received: SigningAlgorithm,
expected: SigningAlgorithm,
},
#[error(transparent)]
Ed25519Recovery(#[from] Ed25519RecoveryError),
#[error(transparent)]
Ed25519SignatureError(SignatureError),
#[error("This command ({name}) has been removed. Please try to use {alternative} instead.")]
RemovedCommand { name: String, alternative: String },
}
@@ -0,0 +1,389 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::BackendError;
use async_trait::async_trait;
use cosmrs::AccountId;
use cosmwasm_std::Addr;
use nym_contracts_common::signing::{
ContractMessageContent, MessageSignature, Nonce, SignableMessage, SigningAlgorithm,
};
use nym_crypto::asymmetric::identity;
use nym_mixnet_contract_common::{
Gateway, GatewayBondingPayload, MixNode, MixNodeCostParams, MixnodeBondingPayload,
};
use validator_client::nyxd::error::NyxdError;
use validator_client::nyxd::traits::MixnetQueryClient;
use validator_client::nyxd::{Coin, SigningNyxdClient};
use validator_client::Client;
type SignableMixNodeBondingMsg = SignableMessage<ContractMessageContent<MixnodeBondingPayload>>;
type SignableGatewayBondingMsg = SignableMessage<ContractMessageContent<GatewayBondingPayload>>;
// define this as a separate trait for mocking purposes
#[async_trait]
pub(crate) trait AddressAndNonceProvider {
async fn get_signing_nonce(&self) -> Result<Nonce, NyxdError>;
fn vesting_contract_address(&self) -> &AccountId;
fn cw_address(&self) -> Addr;
}
#[async_trait]
impl AddressAndNonceProvider for Client<SigningNyxdClient> {
async fn get_signing_nonce(&self) -> Result<Nonce, NyxdError> {
self.nyxd.get_signing_nonce(self.nyxd.address()).await
}
fn vesting_contract_address(&self) -> &AccountId {
self.nyxd.vesting_contract_address()
}
fn cw_address(&self) -> Addr {
self.nyxd.cw_address()
}
}
fn proxy<P: AddressAndNonceProvider>(client: &P, vesting: bool) -> Option<Addr> {
if vesting {
// the call to unchecked is fine here as we're converting directly from `AccountId`
// which must have been a valid bech32 address
Some(Addr::unchecked(client.vesting_contract_address().as_ref()))
} else {
None
}
}
// since the message has to go back to the user due to the increasing nonce, we might as well sign the entire payload
pub(crate) async fn create_mixnode_bonding_sign_payload<P: AddressAndNonceProvider>(
client: &P,
mix_node: MixNode,
cost_params: MixNodeCostParams,
pledge: Coin,
vesting: bool,
) -> Result<SignableMixNodeBondingMsg, BackendError> {
let payload = MixnodeBondingPayload::new(mix_node, cost_params);
let signer = client.cw_address();
let proxy = proxy(client, vesting);
let content = ContractMessageContent::new(signer, proxy, vec![pledge.into()], payload);
let nonce = client.get_signing_nonce().await?;
Ok(SignableMessage::new(nonce, content))
}
pub(crate) async fn verify_mixnode_bonding_sign_payload<P: AddressAndNonceProvider>(
client: &P,
mix_node: &MixNode,
cost_params: &MixNodeCostParams,
pledge: &Coin,
vesting: bool,
msg_signature: &MessageSignature,
) -> Result<(), BackendError> {
let identity_key = identity::PublicKey::from_base58_string(&mix_node.identity_key)?;
let signature = identity::Signature::from_bytes(msg_signature.as_ref())?;
// recreate the plaintext
let msg = create_mixnode_bonding_sign_payload(
client,
mix_node.clone(),
cost_params.clone(),
pledge.clone(),
vesting,
)
.await?;
let plaintext = msg.to_plaintext()?;
if !msg.algorithm.is_ed25519() {
return Err(BackendError::UnexpectedSigningAlgorithm {
received: msg.algorithm,
expected: SigningAlgorithm::Ed25519,
});
}
// TODO: possibly provide better error message if this check fails
identity_key.verify(&plaintext, &signature)?;
Ok(())
}
// since the message has to go back to the user due to the increasing nonce, we might as well sign the entire payload
pub(crate) async fn create_gateway_bonding_sign_payload<P: AddressAndNonceProvider>(
client: &P,
gateway: Gateway,
pledge: Coin,
vesting: bool,
) -> Result<SignableGatewayBondingMsg, BackendError> {
let payload = GatewayBondingPayload::new(gateway);
let signer = client.cw_address();
let proxy = proxy(client, vesting);
let content = ContractMessageContent::new(signer, proxy, vec![pledge.into()], payload);
let nonce = client.get_signing_nonce().await?;
Ok(SignableMessage::new(nonce, content))
}
pub(crate) async fn verify_gateway_bonding_sign_payload<P: AddressAndNonceProvider>(
client: &P,
gateway: &Gateway,
pledge: &Coin,
vesting: bool,
msg_signature: &MessageSignature,
) -> Result<(), BackendError> {
let identity_key = identity::PublicKey::from_base58_string(&gateway.identity_key)?;
let signature = identity::Signature::from_bytes(msg_signature.as_ref())?;
// recreate the plaintext
let msg = create_gateway_bonding_sign_payload(client, gateway.clone(), pledge.clone(), vesting)
.await?;
let plaintext = msg.to_plaintext()?;
if !msg.algorithm.is_ed25519() {
return Err(BackendError::UnexpectedSigningAlgorithm {
received: msg.algorithm,
expected: SigningAlgorithm::Ed25519,
});
}
// TODO: possibly provide better error message if this check fails
identity_key.verify(&plaintext, &signature)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::coin;
use nym_contracts_common::Percent;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
// use rng with constant seed for all tests so that they would be deterministic
pub fn test_rng() -> ChaCha20Rng {
let dummy_seed = [42u8; 32];
rand_chacha::ChaCha20Rng::from_seed(dummy_seed)
}
struct MockClient {
address: Addr,
vesting_contract: AccountId,
signing_nonce: Nonce,
}
#[async_trait]
impl AddressAndNonceProvider for MockClient {
async fn get_signing_nonce(&self) -> Result<Nonce, NyxdError> {
Ok(self.signing_nonce)
}
fn vesting_contract_address(&self) -> &AccountId {
&self.vesting_contract
}
fn cw_address(&self) -> Addr {
self.address.clone()
}
}
#[tokio::test]
async fn dummy_mix_bonding_signature_verification() {
let mut rng = test_rng();
let identity_keypair = identity::KeyPair::new(&mut rng);
let dummy_mixnode = MixNode {
host: "1.2.3.4".to_string(),
mix_port: 1234,
verloc_port: 2345,
http_api_port: 3456,
sphinx_key: "totally-legit-sphinx-key".to_string(),
identity_key: identity_keypair.public_key().to_base58_string(),
version: "v1.2.3".to_string(),
};
let dummy_cost_params = MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(42).unwrap(),
interval_operating_cost: coin(1111111, "unym"),
};
let dummy_pledge: Coin = coin(10000000000, "unym").into();
let dummy_account = Addr::unchecked("n16t2umcd83zjpl5puyuuq6lgmy4p3qedjd8ynn6");
let dummy_client = MockClient {
address: dummy_account,
vesting_contract: "n17tj0a0w6v7r2dc54rnkzfza6s8hxs87rj273a5".parse().unwrap(),
signing_nonce: 42,
};
let signing_msg_liquid = create_mixnode_bonding_sign_payload(
&dummy_client,
dummy_mixnode.clone(),
dummy_cost_params.clone(),
dummy_pledge.clone(),
false,
)
.await
.unwrap();
let plaintext_liquid = signing_msg_liquid.to_plaintext().unwrap();
let sig_liquid: MessageSignature = identity_keypair
.private_key()
.sign(&plaintext_liquid)
.to_bytes()
.as_ref()
.into();
let signing_msg_vesting = create_mixnode_bonding_sign_payload(
&dummy_client,
dummy_mixnode.clone(),
dummy_cost_params.clone(),
dummy_pledge.clone(),
true,
)
.await
.unwrap();
let plaintext_vesting = signing_msg_vesting.to_plaintext().unwrap();
let sig_vesting: MessageSignature = identity_keypair
.private_key()
.sign(&plaintext_vesting)
.to_bytes()
.as_ref()
.into();
let res = verify_mixnode_bonding_sign_payload(
&dummy_client,
&dummy_mixnode,
&dummy_cost_params,
&dummy_pledge,
false,
&sig_liquid,
)
.await;
assert!(res.is_ok());
let res = verify_mixnode_bonding_sign_payload(
&dummy_client,
&dummy_mixnode,
&dummy_cost_params,
&dummy_pledge,
true,
&sig_vesting,
)
.await;
assert!(res.is_ok());
let res = verify_mixnode_bonding_sign_payload(
&dummy_client,
&dummy_mixnode,
&dummy_cost_params,
&dummy_pledge,
false,
&sig_vesting,
)
.await;
assert!(res.is_err());
let res = verify_mixnode_bonding_sign_payload(
&dummy_client,
&dummy_mixnode,
&dummy_cost_params,
&dummy_pledge,
true,
&sig_liquid,
)
.await;
assert!(res.is_err())
}
#[tokio::test]
async fn dummy_gateway_bonding_signature_verification() {
let mut rng = test_rng();
let identity_keypair = identity::KeyPair::new(&mut rng);
let dummy_gateway = Gateway {
host: "1.2.3.4".to_string(),
mix_port: 1234,
clients_port: 2345,
location: "whatever".to_string(),
sphinx_key: "totally-legit-sphinx-key".to_string(),
identity_key: identity_keypair.public_key().to_base58_string(),
version: "v1.2.3".to_string(),
};
let dummy_pledge: Coin = coin(10000000000, "unym").into();
let dummy_account = Addr::unchecked("n16t2umcd83zjpl5puyuuq6lgmy4p3qedjd8ynn6");
let dummy_client = MockClient {
address: dummy_account,
vesting_contract: "n17tj0a0w6v7r2dc54rnkzfza6s8hxs87rj273a5".parse().unwrap(),
signing_nonce: 42,
};
let signing_msg_liquid = create_gateway_bonding_sign_payload(
&dummy_client,
dummy_gateway.clone(),
dummy_pledge.clone(),
false,
)
.await
.unwrap();
let plaintext_liquid = signing_msg_liquid.to_plaintext().unwrap();
let sig_liquid: MessageSignature = identity_keypair
.private_key()
.sign(&plaintext_liquid)
.to_bytes()
.as_ref()
.into();
let signing_msg_vesting = create_gateway_bonding_sign_payload(
&dummy_client,
dummy_gateway.clone(),
dummy_pledge.clone(),
true,
)
.await
.unwrap();
let plaintext_vesting = signing_msg_vesting.to_plaintext().unwrap();
let sig_vesting: MessageSignature = identity_keypair
.private_key()
.sign(&plaintext_vesting)
.to_bytes()
.as_ref()
.into();
let res = verify_gateway_bonding_sign_payload(
&dummy_client,
&dummy_gateway,
&dummy_pledge,
false,
&sig_liquid,
)
.await;
assert!(res.is_ok());
let res = verify_gateway_bonding_sign_payload(
&dummy_client,
&dummy_gateway,
&dummy_pledge,
true,
&sig_vesting,
)
.await;
assert!(res.is_ok());
let res = verify_gateway_bonding_sign_payload(
&dummy_client,
&dummy_gateway,
&dummy_pledge,
false,
&sig_vesting,
)
.await;
assert!(res.is_err());
let res = verify_gateway_bonding_sign_payload(
&dummy_client,
&dummy_gateway,
&dummy_pledge,
true,
&sig_liquid,
)
.await;
assert!(res.is_err())
}
}
@@ -2,8 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::BackendError;
use crate::operations::helpers::verify_mixnode_bonding_sign_payload;
use crate::state::WalletState;
use crate::{nyxd_client, Gateway, MixNode};
use nym_contracts_common::signing::MessageSignature;
use nym_mixnet_contract_common::{MixId, MixNodeConfigUpdate};
use nym_types::currency::DecCoin;
use nym_types::gateway::GatewayBond;
@@ -73,7 +75,7 @@ pub async fn unbond_gateway(
pub async fn bond_mixnode(
mixnode: MixNode,
cost_params: MixNodeCostParams,
owner_signature: String,
msg_signature: MessageSignature,
pledge: DecCoin,
fee: Option<Fee>,
state: tauri::State<'_, WalletState>,
@@ -90,10 +92,31 @@ pub async fn bond_mixnode(
pledge_base,
fee,
);
let res = guard
.current_client()?
let client = guard.current_client()?;
// check the signature to make sure the user copied it correctly
if let Err(err) = verify_mixnode_bonding_sign_payload(
client,
&mixnode,
&cost_params,
&pledge_base,
false,
&msg_signature,
)
.await
{
todo!()
}
let res = client
.nyxd
.bond_mixnode(mixnode, cost_params, owner_signature, pledge_base, fee)
.bond_mixnode(
mixnode,
cost_params,
msg_signature.as_bs58_string(),
pledge_base,
fee,
)
.await?;
log::info!("<<< tx hash = {}", res.transaction_hash);
log::trace!("<<< {:?}", res);
@@ -1,4 +1,8 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod help;
pub(crate) mod helpers;
pub mod mixnet;
pub mod nym_api;
pub mod signatures;