A pull request for discussion about contract state variables (#563)
* A pull request for discussion about contract state variables * Updated contract state struct * Introduced transaction to update the contract state * Moved transactions to separate file * Corrected return type on query * Corrected query match * Added test for state params query * Test for state params update * Removed unused imports * Helper functions to query for state params * Removed hardcoded inside bond, old mix bond value * Helper function to update contract state * Changed typescript StateParam from numbers to strings * Somehow unresolved post-merge issues in tests * Introduced additional helpers for the validator client to use the new contract features * Using the state-specified bond values Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
This commit is contained in:
@@ -30,6 +30,11 @@ export function printableBalanceToNative(amountToDisplay: string): string {
|
||||
return decimalAmount.atomics;
|
||||
}
|
||||
|
||||
// reciprocal of `printableBalanceToNative`, takes, for example 10000000 and returns 10
|
||||
export function nativeToPrintable(nativeValue: string): string {
|
||||
return Decimal.fromAtomics(nativeValue, 6).toString()
|
||||
}
|
||||
|
||||
export interface MappedCoin {
|
||||
readonly denom: string;
|
||||
readonly fractionalDigits: number;
|
||||
|
||||
@@ -5,18 +5,35 @@ import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
|
||||
import MixnodesCache from "./caches/mixnodes";
|
||||
import { coin, Coin, coins } from "@cosmjs/launchpad";
|
||||
import { BroadcastTxResponse } from "@cosmjs/stargate"
|
||||
import { ExecuteResult, InstantiateOptions, InstantiateResult, MigrateResult, UploadMeta, UploadResult } from "@cosmjs/cosmwasm";
|
||||
import { CoinMap, displayAmountToNative, MappedCoin, nativeCoinToDisplay, printableBalance, printableCoin } from "./currency";
|
||||
import {
|
||||
ExecuteResult,
|
||||
InstantiateOptions,
|
||||
InstantiateResult,
|
||||
MigrateResult,
|
||||
UploadMeta,
|
||||
UploadResult
|
||||
} from "@cosmjs/cosmwasm";
|
||||
import {
|
||||
CoinMap,
|
||||
displayAmountToNative,
|
||||
MappedCoin,
|
||||
nativeCoinToDisplay,
|
||||
printableBalance,
|
||||
printableCoin,
|
||||
nativeToPrintable
|
||||
} from "./currency";
|
||||
import GatewaysCache from "./caches/gateways";
|
||||
import QueryClient, { IQueryClient } from "./query-client";
|
||||
|
||||
export { coins, coin };
|
||||
export { Coin };
|
||||
export { displayAmountToNative, nativeCoinToDisplay, printableCoin, printableBalance, MappedCoin, CoinMap }
|
||||
export { displayAmountToNative, nativeCoinToDisplay, printableCoin, printableBalance, nativeToPrintable, MappedCoin, CoinMap }
|
||||
|
||||
export default class ValidatorClient {
|
||||
private readonly stakeDenom: string;
|
||||
private readonly gatewayBondingStake: number = 1000_000000
|
||||
private readonly defaultGatewayBondingStake: number = 100_000000
|
||||
private readonly defaultMixnodeBondingStake: number = 100_000000
|
||||
|
||||
url: string;
|
||||
private readonly client: INetClient | IQueryClient
|
||||
private mixNodesCache: MixnodesCache;
|
||||
@@ -84,7 +101,7 @@ export default class ValidatorClient {
|
||||
*/
|
||||
static async mnemonicToAddress(mnemonic: string): Promise<string> {
|
||||
const wallet = await ValidatorClient.buildWallet(mnemonic);
|
||||
const [{ address }] = await wallet.getAccounts()
|
||||
const [{address}] = await wallet.getAccounts()
|
||||
return address
|
||||
}
|
||||
|
||||
@@ -96,6 +113,11 @@ export default class ValidatorClient {
|
||||
return this.client.getBalance(address, this.stakeDenom);
|
||||
}
|
||||
|
||||
async getStateParams(): Promise<StateParams> {
|
||||
return this.client.getStateParams(this.contractAddress)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get or refresh the list of mixnodes in the network.
|
||||
*
|
||||
@@ -118,11 +140,20 @@ export default class ValidatorClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce a mixnode, paying a fee.
|
||||
*/
|
||||
* Generate a minimum gateway bond required to create a fresh mixnode.
|
||||
*
|
||||
* @returns a `Coin` instance containing minimum amount of coins to stake a gateway.
|
||||
*/
|
||||
minimumMixnodeBond = (): Coin => {
|
||||
return coin(this.defaultMixnodeBondingStake, this.stakeDenom)
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce a mixnode, paying a fee.
|
||||
*/
|
||||
async bond(mixNode: MixNode): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
const bond = [{ amount: "1000000000", denom: this.stakeDenom }];
|
||||
const bond = [this.minimumMixnodeBond()];
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { bond_mixnode: { mix_node: mixNode } }, "adding mixnode", bond);
|
||||
console.log(`account ${this.client.clientAddress} added mixnode with ${mixNode.host}`);
|
||||
return result;
|
||||
@@ -195,7 +226,7 @@ export default class ValidatorClient {
|
||||
* @returns a `Coin` instance containing minimum amount of coins to stake a gateway.
|
||||
*/
|
||||
minimumGatewayBond = (): Coin => {
|
||||
return coin(this.gatewayBondingStake, this.stakeDenom)
|
||||
return coin(this.defaultGatewayBondingStake, this.stakeDenom)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,7 +235,7 @@ export default class ValidatorClient {
|
||||
async bondGateway(gateway: Gateway): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
const bond = this.minimumGatewayBond()
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { bond_gateway: { gateway: gateway } }, "adding gateway", [bond]);
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {bond_gateway: {gateway: gateway}}, "adding gateway", [bond]);
|
||||
console.log(`account ${this.client.clientAddress} added gateway with ${gateway.mix_host}`);
|
||||
return result;
|
||||
} else {
|
||||
@@ -217,7 +248,7 @@ export default class ValidatorClient {
|
||||
*/
|
||||
async unbondGateway(): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { unbond_gateway: {} })
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {unbond_gateway: {}})
|
||||
console.log(`account ${this.client.clientAddress} unbonded gateway`);
|
||||
return result;
|
||||
} else {
|
||||
@@ -225,6 +256,14 @@ export default class ValidatorClient {
|
||||
}
|
||||
}
|
||||
|
||||
async updateStateParams(newParams: StateParams): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
return await this.client.executeContract(this.client.clientAddress, this.contractAddress, {update_state_params: newParams}, "updating contract state");
|
||||
} else {
|
||||
throw new Error("Tried to update state params with a query client")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO: if we just keep a reference to the SigningCosmWasmClient somewhere we can probably go direct
|
||||
// to it in the case of these methods below.
|
||||
@@ -306,3 +345,14 @@ export type GatewayOwnershipResponse = {
|
||||
address: string,
|
||||
has_gateway: boolean,
|
||||
}
|
||||
|
||||
export type StateParams = {
|
||||
// ideally I'd want to define those as `number` rather than `string`, but
|
||||
// rust-side they are defined as Uint128 and Decimal that don't have
|
||||
// native javascript representations and therefore are interpreted as strings after deserialization
|
||||
minimum_mixnode_bond: string,
|
||||
minimum_gateway_bond: string,
|
||||
mixnode_bond_reward_rate: string,
|
||||
gateway_bond_reward_rate: string,
|
||||
mixnode_active_set_size: number,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { SigningCosmWasmClient, SigningCosmWasmClientOptions } from "@cosmjs/cosmwasm-stargate";
|
||||
import {GatewayOwnershipResponse, MixOwnershipResponse, PagedGatewayResponse, PagedResponse} from "./index";
|
||||
import {
|
||||
GatewayOwnershipResponse,
|
||||
MixOwnershipResponse,
|
||||
PagedGatewayResponse,
|
||||
PagedResponse,
|
||||
StateParams
|
||||
} from "./index";
|
||||
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
|
||||
import { Coin, GasPrice } from "@cosmjs/launchpad";
|
||||
import { BroadcastTxResponse } from "@cosmjs/stargate"
|
||||
@@ -14,6 +20,7 @@ export interface INetClient {
|
||||
getGateways(contractAddress: string, limit: number, start_after?: string): Promise<PagedGatewayResponse>;
|
||||
ownsMixNode(contractAddress: string, address: string): Promise<MixOwnershipResponse>;
|
||||
ownsGateway(contractAddress: string, address: string): Promise<GatewayOwnershipResponse>;
|
||||
getStateParams(contractAddress: string): Promise<StateParams>;
|
||||
executeContract(senderAddress: string, contractAddress: string, handleMsg: Record<string, unknown>, memo?: string, transferAmount?: readonly Coin[]): Promise<ExecuteResult>;
|
||||
instantiate(senderAddress: string, codeId: number, initMsg: Record<string, unknown>, label: string, options?: InstantiateOptions): Promise<InstantiateResult>;
|
||||
sendTokens(senderAddress: string, recipientAddress: string, transferAmount: readonly Coin[], memo?: string): Promise<BroadcastTxResponse>;
|
||||
@@ -77,6 +84,10 @@ export default class NetClient implements INetClient {
|
||||
return this.cosmClient.getBalance(address, stakeDenom);
|
||||
}
|
||||
|
||||
public getStateParams(contractAddress: string): Promise<StateParams> {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { state_params: { } });
|
||||
}
|
||||
|
||||
public executeContract(senderAddress: string, contractAddress: string, handleMsg: Record<string, unknown>, memo?: string, transferAmount?: readonly Coin[]): Promise<ExecuteResult> {
|
||||
return this.cosmClient.execute(senderAddress, contractAddress, handleMsg, memo, transferAmount);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import {Coin} from "@cosmjs/launchpad";
|
||||
import {CosmWasmClient} from "@cosmjs/cosmwasm-stargate";
|
||||
import {GatewayOwnershipResponse, MixOwnershipResponse, PagedGatewayResponse, PagedResponse} from "./index";
|
||||
import {
|
||||
GatewayOwnershipResponse,
|
||||
MixOwnershipResponse,
|
||||
PagedGatewayResponse,
|
||||
PagedResponse,
|
||||
StateParams
|
||||
} from "./index";
|
||||
|
||||
export interface IQueryClient {
|
||||
getBalance(address: string, stakeDenom: string): Promise<Coin | null>;
|
||||
@@ -8,6 +14,7 @@ export interface IQueryClient {
|
||||
getGateways(contractAddress: string, limit: number, start_after?: string): Promise<PagedGatewayResponse>;
|
||||
ownsMixNode(contractAddress: string, address: string): Promise<MixOwnershipResponse>;
|
||||
ownsGateway(contractAddress: string, address: string): Promise<GatewayOwnershipResponse>;
|
||||
getStateParams(contractAddress: string): Promise<StateParams>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,4 +64,8 @@ export default class QueryClient implements IQueryClient {
|
||||
public getBalance(address: string, stakeDenom: string): Promise<Coin | null> {
|
||||
return this.cosmClient.getBalance(address, stakeDenom);
|
||||
}
|
||||
|
||||
public getStateParams(contractAddress: string): Promise<StateParams> {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { state_params: { } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
use crate::msg::{HandleMsg, InitMsg, MigrateMsg, QueryMsg};
|
||||
use crate::queries::{
|
||||
query_gateways_paged, query_mixnodes_paged, query_owns_gateway, query_owns_mixnode,
|
||||
};
|
||||
use crate::state::{config, gateways, gateways_read, State};
|
||||
use crate::{error::ContractError, state::mixnodes, state::mixnodes_read};
|
||||
use crate::state::{config, State, StateParams};
|
||||
use crate::{error::ContractError, queries, transactions};
|
||||
use cosmwasm_std::{
|
||||
attr, to_binary, BankMsg, Binary, Coin, Deps, DepsMut, Env, HandleResponse, InitResponse,
|
||||
MessageInfo, MigrateResponse, StdResult, Uint128,
|
||||
to_binary, Decimal, Deps, DepsMut, Env, HandleResponse, InitResponse, MessageInfo,
|
||||
MigrateResponse, QueryResponse, Uint128,
|
||||
};
|
||||
use mixnet_contract::{Gateway, GatewayBond, MixNode, MixNodeBond};
|
||||
|
||||
/// Constant specifying minimum of coin required to bond a gateway
|
||||
const GATEWAY_BOND: Uint128 = Uint128(100_000000);
|
||||
pub const INITIAL_GATEWAY_BOND: Uint128 = Uint128(100_000000);
|
||||
|
||||
/// Constant specifying minimum of coin required to bond a mixnode
|
||||
const MIXNODE_BOND: Uint128 = Uint128(100_000000);
|
||||
pub const INITIAL_MIXNODE_BOND: Uint128 = Uint128(100_000000);
|
||||
|
||||
pub const INITIAL_MIXNODE_BOND_REWARD_RATE: Decimal = Decimal::one();
|
||||
|
||||
pub const INITIAL_GATEWAY_BOND_REWARD_RATE: Decimal = Decimal::one();
|
||||
|
||||
pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100;
|
||||
|
||||
/// Constant specifying denomination of the coin used for bonding
|
||||
pub const DENOM: &str = "uhal";
|
||||
@@ -30,7 +32,19 @@ pub fn init(
|
||||
info: MessageInfo,
|
||||
_msg: InitMsg,
|
||||
) -> Result<InitResponse, ContractError> {
|
||||
let state = State { owner: info.sender };
|
||||
// TODO: to discuss with DH, should the initial state be set as it is right now, i.e.
|
||||
// using the defined constants, or should it rather be all based on whatever is sent
|
||||
// in `InitMsg`?
|
||||
let state = State {
|
||||
owner: info.sender,
|
||||
params: StateParams {
|
||||
minimum_mixnode_bond: INITIAL_MIXNODE_BOND,
|
||||
minimum_gateway_bond: INITIAL_GATEWAY_BOND,
|
||||
mixnode_bond_reward_rate: INITIAL_MIXNODE_BOND_REWARD_RATE,
|
||||
gateway_bond_reward_rate: INITIAL_GATEWAY_BOND_REWARD_RATE,
|
||||
mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE,
|
||||
},
|
||||
};
|
||||
config(deps.storage).save(&state)?;
|
||||
Ok(InitResponse::default())
|
||||
}
|
||||
@@ -43,201 +57,34 @@ pub fn handle(
|
||||
msg: HandleMsg,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
match msg {
|
||||
HandleMsg::BondMixnode { mix_node } => try_add_mixnode(deps, info, mix_node),
|
||||
HandleMsg::UnbondMixnode {} => try_remove_mixnode(deps, info, env),
|
||||
HandleMsg::BondGateway { gateway } => try_add_gateway(deps, info, gateway),
|
||||
HandleMsg::UnbondGateway {} => try_remove_gateway(deps, info, env),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_mixnode_bond(bond: &[Coin]) -> Result<(), ContractError> {
|
||||
// check if anything was put as bond
|
||||
if bond.is_empty() {
|
||||
return Err(ContractError::NoBondFound);
|
||||
}
|
||||
|
||||
if bond.len() > 1 {
|
||||
// TODO: ask DH what would be an appropriate action here
|
||||
}
|
||||
|
||||
// check that the denomination is correct
|
||||
if bond[0].denom != DENOM {
|
||||
return Err(ContractError::WrongDenom {});
|
||||
}
|
||||
|
||||
// check that we have at least MIXNODE_BOND coins in our bond
|
||||
if bond[0].amount < MIXNODE_BOND {
|
||||
return Err(ContractError::InsufficientMixNodeBond {
|
||||
received: bond[0].amount.into(),
|
||||
minimum: GATEWAY_BOND.into(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn try_add_mixnode(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
mix_node: MixNode,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
validate_mixnode_bond(&info.sent_funds)?;
|
||||
|
||||
let bond = MixNodeBond::new(info.sent_funds, info.sender.clone(), mix_node);
|
||||
|
||||
let sender_bytes = info.sender.as_bytes();
|
||||
let was_present = mixnodes_read(deps.storage)
|
||||
.may_load(sender_bytes)?
|
||||
.is_some();
|
||||
|
||||
// TODO: do attributes also go back to the client or does this need to be put into `data`?
|
||||
let attributes = vec![attr("overwritten", was_present)];
|
||||
|
||||
mixnodes(deps.storage).save(sender_bytes, &bond)?;
|
||||
|
||||
Ok(HandleResponse {
|
||||
messages: vec![],
|
||||
attributes,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn try_remove_mixnode(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
env: Env,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
// find the bond, return ContractError::MixNodeBondNotFound if it doesn't exist
|
||||
let mixnode_bond = match mixnodes_read(deps.storage).may_load(info.sender.as_bytes())? {
|
||||
None => return Err(ContractError::MixNodeBondNotFound {}),
|
||||
Some(bond) => bond,
|
||||
};
|
||||
|
||||
// send bonded funds back to the bond owner
|
||||
let messages = vec![BankMsg::Send {
|
||||
from_address: env.contract.address,
|
||||
to_address: info.sender.clone(),
|
||||
amount: mixnode_bond.amount().to_vec(),
|
||||
}
|
||||
.into()];
|
||||
|
||||
// remove the bond from the list of bonded mixnodes
|
||||
mixnodes(deps.storage).remove(info.sender.as_bytes());
|
||||
|
||||
// log our actions
|
||||
let attributes = vec![attr("action", "unbond"), attr("mixnode_bond", mixnode_bond)];
|
||||
|
||||
Ok(HandleResponse {
|
||||
messages,
|
||||
attributes,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_gateway_bond(bond: &[Coin]) -> Result<(), ContractError> {
|
||||
// check if anything was put as bond
|
||||
if bond.is_empty() {
|
||||
return Err(ContractError::NoBondFound);
|
||||
}
|
||||
|
||||
if bond.len() > 1 {
|
||||
// TODO: ask DH what would be an appropriate action here
|
||||
}
|
||||
|
||||
// check that the denomination is correct
|
||||
if bond[0].denom != DENOM {
|
||||
return Err(ContractError::WrongDenom {});
|
||||
}
|
||||
|
||||
// check that we have at least 100 coins in our bond
|
||||
if bond[0].amount < GATEWAY_BOND {
|
||||
return Err(ContractError::InsufficientGatewayBond {
|
||||
received: bond[0].amount.into(),
|
||||
minimum: GATEWAY_BOND.into(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn try_add_gateway(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
gateway: Gateway,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
validate_gateway_bond(&info.sent_funds)?;
|
||||
|
||||
let bond = GatewayBond::new(info.sent_funds, info.sender.clone(), gateway);
|
||||
|
||||
let sender_bytes = info.sender.as_bytes();
|
||||
let was_present = gateways_read(deps.storage)
|
||||
.may_load(sender_bytes)?
|
||||
.is_some();
|
||||
|
||||
// TODO: do attributes also go back to the client or does this need to be put into `data`?
|
||||
let attributes = vec![attr("overwritten", was_present)];
|
||||
|
||||
gateways(deps.storage).save(sender_bytes, &bond)?;
|
||||
Ok(HandleResponse {
|
||||
messages: vec![],
|
||||
attributes,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn try_remove_gateway(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
env: Env,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
let sender_bytes = info.sender.as_bytes();
|
||||
|
||||
// find the bond, return ContractError::GatewayBondNotFound if it doesn't exist
|
||||
let gateway_bond = match gateways_read(deps.storage).may_load(sender_bytes)? {
|
||||
None => {
|
||||
return Err(ContractError::GatewayBondNotFound {
|
||||
account: info.sender,
|
||||
});
|
||||
HandleMsg::BondMixnode { mix_node } => transactions::try_add_mixnode(deps, info, mix_node),
|
||||
HandleMsg::UnbondMixnode {} => transactions::try_remove_mixnode(deps, info, env),
|
||||
HandleMsg::BondGateway { gateway } => transactions::try_add_gateway(deps, info, gateway),
|
||||
HandleMsg::UnbondGateway {} => transactions::try_remove_gateway(deps, info, env),
|
||||
HandleMsg::UpdateStateParams(params) => {
|
||||
transactions::try_update_state_params(deps, info, params)
|
||||
}
|
||||
Some(bond) => bond,
|
||||
};
|
||||
|
||||
// send bonded funds back to the bond owner
|
||||
let messages = vec![BankMsg::Send {
|
||||
from_address: env.contract.address,
|
||||
to_address: info.sender.clone(),
|
||||
amount: gateway_bond.amount().to_vec(),
|
||||
}
|
||||
.into()];
|
||||
|
||||
// remove the bond from the list of bonded gateways
|
||||
gateways(deps.storage).remove(sender_bytes);
|
||||
|
||||
// log our actions
|
||||
let attributes = vec![
|
||||
attr("action", "unbond"),
|
||||
attr("address", info.sender),
|
||||
attr("gateway_bond", gateway_bond),
|
||||
];
|
||||
|
||||
Ok(HandleResponse {
|
||||
messages,
|
||||
attributes,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
|
||||
match msg {
|
||||
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
|
||||
let query_res = match msg {
|
||||
QueryMsg::GetMixNodes { start_after, limit } => {
|
||||
to_binary(&query_mixnodes_paged(deps, start_after, limit)?)
|
||||
to_binary(&queries::query_mixnodes_paged(deps, start_after, limit)?)
|
||||
}
|
||||
QueryMsg::GetGateways { limit, start_after } => {
|
||||
to_binary(&query_gateways_paged(deps, start_after, limit)?)
|
||||
to_binary(&queries::query_gateways_paged(deps, start_after, limit)?)
|
||||
}
|
||||
QueryMsg::OwnsMixnode { address } => to_binary(&query_owns_mixnode(deps, address)?),
|
||||
QueryMsg::OwnsGateway { address } => to_binary(&query_owns_gateway(deps, address)?),
|
||||
}
|
||||
QueryMsg::OwnsMixnode { address } => {
|
||||
to_binary(&queries::query_owns_mixnode(deps, address)?)
|
||||
}
|
||||
QueryMsg::OwnsGateway { address } => {
|
||||
to_binary(&queries::query_owns_gateway(deps, address)?)
|
||||
}
|
||||
QueryMsg::StateParams {} => to_binary(&queries::query_state_params(deps)),
|
||||
};
|
||||
|
||||
Ok(query_res?)
|
||||
}
|
||||
|
||||
pub fn migrate(
|
||||
@@ -252,11 +99,10 @@ pub fn migrate(
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::support::tests::helpers;
|
||||
use crate::support::tests::helpers::*;
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
|
||||
use cosmwasm_std::{coins, from_binary};
|
||||
use mixnet_contract::{PagedGatewayResponse, PagedResponse};
|
||||
use mixnet_contract::PagedResponse;
|
||||
|
||||
#[test]
|
||||
fn initialize_contract() {
|
||||
@@ -287,419 +133,4 @@ pub mod tests {
|
||||
query_contract_balance(env.contract.address, deps)
|
||||
);
|
||||
}
|
||||
|
||||
fn good_mixnode_bond() -> Vec<Coin> {
|
||||
vec![Coin {
|
||||
denom: DENOM.to_string(),
|
||||
amount: MIXNODE_BOND,
|
||||
}]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validating_mixnode_bond() {
|
||||
// you must send SOME funds
|
||||
let result = validate_mixnode_bond(&[]);
|
||||
assert_eq!(result, Err(ContractError::NoBondFound));
|
||||
|
||||
// you must send at least 100 coins...
|
||||
let mut bond = good_mixnode_bond();
|
||||
bond[0].amount = (MIXNODE_BOND - Uint128(1)).unwrap();
|
||||
let result = validate_mixnode_bond(&bond);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::InsufficientMixNodeBond {
|
||||
received: Into::<u128>::into(MIXNODE_BOND) - 1,
|
||||
minimum: MIXNODE_BOND.into(),
|
||||
})
|
||||
);
|
||||
|
||||
// more than that is still fine
|
||||
let mut bond = good_mixnode_bond();
|
||||
bond[0].amount = MIXNODE_BOND + Uint128(1);
|
||||
let result = validate_mixnode_bond(&bond);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// it must be sent in the defined denom!
|
||||
let mut bond = good_mixnode_bond();
|
||||
bond[0].denom = "baddenom".to_string();
|
||||
let result = validate_mixnode_bond(&bond);
|
||||
assert_eq!(result, Err(ContractError::WrongDenom {}));
|
||||
|
||||
let mut bond = good_mixnode_bond();
|
||||
bond[0].denom = "foomp".to_string();
|
||||
let result = validate_mixnode_bond(&bond);
|
||||
assert_eq!(result, Err(ContractError::WrongDenom {}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnode_add() {
|
||||
let mut deps = helpers::init_contract();
|
||||
|
||||
// if we don't send enough funds
|
||||
let insufficient_bond = Into::<u128>::into(MIXNODE_BOND) - 1;
|
||||
let info = mock_info("anyone", &coins(insufficient_bond, DENOM));
|
||||
let msg = HandleMsg::BondMixnode {
|
||||
mix_node: helpers::mix_node_fixture(),
|
||||
};
|
||||
|
||||
// we are informed that we didn't send enough funds
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::InsufficientMixNodeBond {
|
||||
received: insufficient_bond,
|
||||
minimum: GATEWAY_BOND.into(),
|
||||
})
|
||||
);
|
||||
|
||||
// no mixnode was inserted into the topology
|
||||
let res = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedResponse = from_binary(&res).unwrap();
|
||||
assert_eq!(0, page.nodes.len());
|
||||
|
||||
// if we send enough funds
|
||||
let info = mock_info("anyone", &coins(1000_000000, DENOM));
|
||||
let msg = HandleMsg::BondMixnode {
|
||||
mix_node: helpers::mix_node_fixture(),
|
||||
};
|
||||
|
||||
// we get back a message telling us everything was OK
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert!(handle_response.is_ok());
|
||||
|
||||
// we can query topology and the new node is there
|
||||
let query_response = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedResponse = from_binary(&query_response).unwrap();
|
||||
assert_eq!(1, page.nodes.len());
|
||||
assert_eq!(&helpers::mix_node_fixture(), page.nodes[0].mix_node());
|
||||
|
||||
// if there was already a mixnode bonded by particular user
|
||||
let info = mock_info("foomper", &good_mixnode_bond());
|
||||
let msg = HandleMsg::BondGateway {
|
||||
gateway: helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(handle_response.attributes[0], attr("overwritten", false));
|
||||
|
||||
let info = mock_info("foomper", &good_mixnode_bond());
|
||||
let msg = HandleMsg::BondGateway {
|
||||
gateway: helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
// we get a log message about it (TODO: does it get back to the user?)
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(handle_response.attributes[0], attr("overwritten", true));
|
||||
|
||||
// adding another node from another account, but with the same IP, should fail (or we would have a weird state). Is that right? Think about this, not sure yet.
|
||||
// if we attempt to register a second node from the same address, should we get an error? It would probably be polite.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnode_remove() {
|
||||
let env = mock_env();
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let msg = InitMsg {};
|
||||
let info = mock_info("creator", &[]);
|
||||
init(deps.as_mut(), env.clone(), info, msg).unwrap();
|
||||
|
||||
// try un-registering when no nodes exist yet
|
||||
let info = mock_info("anyone", &coins(999_9999, DENOM));
|
||||
let msg = HandleMsg::UnbondMixnode {};
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
|
||||
// we're told that there is no node for our address
|
||||
assert_eq!(result, Err(ContractError::MixNodeBondNotFound {}));
|
||||
|
||||
// let's add a node owned by bob
|
||||
helpers::add_mixnode("bob", coins(1000_000000, DENOM), &mut deps);
|
||||
|
||||
// attempt to un-register fred's node, which doesn't exist
|
||||
let info = mock_info("fred", &coins(999_9999, DENOM));
|
||||
let msg = HandleMsg::UnbondMixnode {};
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(result, Err(ContractError::MixNodeBondNotFound {}));
|
||||
|
||||
// bob's node is still there
|
||||
let res = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedResponse = from_binary(&res).unwrap();
|
||||
let first_node = &page.nodes[0];
|
||||
assert_eq!(1, page.nodes.len());
|
||||
assert_eq!("bob", first_node.owner());
|
||||
|
||||
// add a node owned by fred
|
||||
let fred_bond = good_mixnode_bond();
|
||||
helpers::add_mixnode("fred", fred_bond.clone(), &mut deps);
|
||||
|
||||
// let's make sure we now have 2 nodes:
|
||||
assert_eq!(2, helpers::get_mix_nodes(&mut deps).len());
|
||||
|
||||
// un-register fred's node
|
||||
let info = mock_info("fred", &coins(999_9999, DENOM));
|
||||
let msg = HandleMsg::UnbondMixnode {};
|
||||
let remove_fred = handle(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
|
||||
|
||||
// we should see log messages come back showing an unbond message
|
||||
let expected_attributes = vec![
|
||||
attr("action", "unbond"),
|
||||
attr(
|
||||
"mixnode_bond",
|
||||
format!("amount: {} {}, owner: fred", MIXNODE_BOND, DENOM),
|
||||
),
|
||||
];
|
||||
|
||||
// we should see a funds transfer from the contract back to fred
|
||||
let expected_messages = vec![BankMsg::Send {
|
||||
from_address: env.contract.address,
|
||||
to_address: info.sender,
|
||||
amount: fred_bond,
|
||||
}
|
||||
.into()];
|
||||
|
||||
// run the handler and check that we got back the correct results
|
||||
let expected = HandleResponse {
|
||||
messages: expected_messages,
|
||||
attributes: expected_attributes,
|
||||
data: None,
|
||||
};
|
||||
assert_eq!(remove_fred, expected);
|
||||
|
||||
// only 1 node now exists, owned by bob:
|
||||
let mix_node_bonds = helpers::get_mix_nodes(&mut deps);
|
||||
assert_eq!(1, mix_node_bonds.len());
|
||||
assert_eq!("bob", mix_node_bonds[0].owner());
|
||||
}
|
||||
|
||||
fn good_gateway_bond() -> Vec<Coin> {
|
||||
vec![Coin {
|
||||
denom: DENOM.to_string(),
|
||||
amount: GATEWAY_BOND,
|
||||
}]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validating_gateway_bond() {
|
||||
// you must send SOME funds
|
||||
let result = validate_gateway_bond(&[]);
|
||||
assert_eq!(result, Err(ContractError::NoBondFound));
|
||||
|
||||
// you must send at least 100 coins...
|
||||
let mut bond = good_gateway_bond();
|
||||
bond[0].amount = (GATEWAY_BOND - Uint128(1)).unwrap();
|
||||
let result = validate_gateway_bond(&bond);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::InsufficientGatewayBond {
|
||||
received: Into::<u128>::into(GATEWAY_BOND) - 1,
|
||||
minimum: GATEWAY_BOND.into(),
|
||||
})
|
||||
);
|
||||
|
||||
// more than that is still fine
|
||||
let mut bond = good_gateway_bond();
|
||||
bond[0].amount = GATEWAY_BOND + Uint128(1);
|
||||
let result = validate_gateway_bond(&bond);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// it must be sent in the defined denom!
|
||||
let mut bond = good_gateway_bond();
|
||||
bond[0].denom = "baddenom".to_string();
|
||||
let result = validate_gateway_bond(&bond);
|
||||
assert_eq!(result, Err(ContractError::WrongDenom {}));
|
||||
|
||||
let mut bond = good_gateway_bond();
|
||||
bond[0].denom = "foomp".to_string();
|
||||
let result = validate_gateway_bond(&bond);
|
||||
assert_eq!(result, Err(ContractError::WrongDenom {}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_add() {
|
||||
let mut deps = helpers::init_contract();
|
||||
|
||||
// if we fail validation (by say not sending enough funds
|
||||
let insufficient_bond = Into::<u128>::into(GATEWAY_BOND) - 1;
|
||||
let info = mock_info("anyone", &coins(insufficient_bond, DENOM));
|
||||
let msg = HandleMsg::BondGateway {
|
||||
gateway: helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
// we are informed that we didn't send enough funds
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::InsufficientGatewayBond {
|
||||
received: insufficient_bond,
|
||||
minimum: GATEWAY_BOND.into(),
|
||||
})
|
||||
);
|
||||
|
||||
// make sure no gateway was inserted into the topology
|
||||
let res = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetGateways {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedGatewayResponse = from_binary(&res).unwrap();
|
||||
assert_eq!(0, page.nodes.len());
|
||||
|
||||
// if we send enough funds
|
||||
let info = mock_info("anyone", &good_gateway_bond());
|
||||
let msg = HandleMsg::BondGateway {
|
||||
gateway: helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
// we get back a message telling us everything was OK
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert!(handle_response.is_ok());
|
||||
|
||||
// we can query topology and the new node is there
|
||||
let query_response = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetGateways {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedGatewayResponse = from_binary(&query_response).unwrap();
|
||||
assert_eq!(1, page.nodes.len());
|
||||
assert_eq!(&helpers::gateway_fixture(), page.nodes[0].gateway());
|
||||
|
||||
// if there was already a gateway bonded by particular user
|
||||
let info = mock_info("foomper", &good_gateway_bond());
|
||||
let msg = HandleMsg::BondGateway {
|
||||
gateway: helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(handle_response.attributes[0], attr("overwritten", false));
|
||||
|
||||
let info = mock_info("foomper", &good_gateway_bond());
|
||||
let msg = HandleMsg::BondGateway {
|
||||
gateway: helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
// we get a log message about it (TODO: does it get back to the user?)
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(handle_response.attributes[0], attr("overwritten", true));
|
||||
|
||||
// adding another node from another account, but with the same IP, should fail (or we would have a weird state).
|
||||
// Is that right? Think about this, not sure yet.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_remove() {
|
||||
let env = mock_env();
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let msg = InitMsg {};
|
||||
let info = mock_info("creator", &[]);
|
||||
init(deps.as_mut(), env.clone(), info, msg).unwrap();
|
||||
|
||||
// try unbond when no nodes exist yet
|
||||
let info = mock_info("anyone", &[]);
|
||||
let msg = HandleMsg::UnbondGateway {};
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
|
||||
// we're told that there is no node for our address
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::GatewayBondNotFound {
|
||||
account: "anyone".into()
|
||||
})
|
||||
);
|
||||
|
||||
// let's add a node owned by bob
|
||||
helpers::add_gateway("bob", good_gateway_bond(), &mut deps);
|
||||
|
||||
// attempt to unbond fred's node, which doesn't exist
|
||||
let info = mock_info("fred", &[]);
|
||||
let msg = HandleMsg::UnbondGateway {};
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::GatewayBondNotFound {
|
||||
account: "fred".into()
|
||||
})
|
||||
);
|
||||
|
||||
// bob's node is still there
|
||||
let nodes = helpers::get_gateways(&mut deps);
|
||||
assert_eq!(1, nodes.len());
|
||||
|
||||
let first_node = &nodes[0];
|
||||
assert_eq!("bob", first_node.owner());
|
||||
|
||||
// add a node owned by fred
|
||||
let fred_bond = good_gateway_bond();
|
||||
helpers::add_gateway("fred", fred_bond.clone(), &mut deps);
|
||||
|
||||
// let's make sure we now have 2 nodes:
|
||||
assert_eq!(2, helpers::get_gateways(&mut deps).len());
|
||||
|
||||
// unbond fred's node
|
||||
let info = mock_info("fred", &[]);
|
||||
let msg = HandleMsg::UnbondGateway {};
|
||||
let remove_fred = handle(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
|
||||
|
||||
// we should see log messages come back showing an unbond message
|
||||
let expected_attributes = vec![
|
||||
attr("action", "unbond"),
|
||||
attr("address", "fred"),
|
||||
attr(
|
||||
"gateway_bond",
|
||||
format!("amount: {} {}, owner: fred", GATEWAY_BOND, DENOM),
|
||||
),
|
||||
];
|
||||
|
||||
// we should see a funds transfer from the contract back to fred
|
||||
let expected_messages = vec![BankMsg::Send {
|
||||
from_address: env.contract.address,
|
||||
to_address: info.sender,
|
||||
amount: fred_bond,
|
||||
}
|
||||
.into()];
|
||||
|
||||
// run the handler and check that we got back the correct results
|
||||
let expected = HandleResponse {
|
||||
messages: expected_messages,
|
||||
attributes: expected_attributes,
|
||||
data: None,
|
||||
};
|
||||
assert_eq!(remove_fred, expected);
|
||||
|
||||
// only 1 node now exists, owned by bob:
|
||||
let gateway_bonds = helpers::get_gateways(&mut deps);
|
||||
assert_eq!(1, gateway_bonds.len());
|
||||
assert_eq!("bob", gateway_bonds[0].owner());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ pub enum ContractError {
|
||||
GatewayBondNotFound { account: HumanAddr },
|
||||
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized {},
|
||||
Unauthorized,
|
||||
|
||||
#[error("Wrong coin denomination, you must send {}", DENOM)]
|
||||
WrongDenom {},
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod msg;
|
||||
pub mod queries;
|
||||
pub mod state;
|
||||
pub mod support;
|
||||
pub mod transactions;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
cosmwasm_std::create_entry_points_with_migration!(contract);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::state::StateParams;
|
||||
use cosmwasm_std::HumanAddr;
|
||||
use mixnet_contract::{Gateway, MixNode};
|
||||
use schemars::JsonSchema;
|
||||
@@ -13,6 +14,7 @@ pub enum HandleMsg {
|
||||
UnbondMixnode {},
|
||||
BondGateway { gateway: Gateway },
|
||||
UnbondGateway {},
|
||||
UpdateStateParams(StateParams),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
@@ -32,6 +34,7 @@ pub enum QueryMsg {
|
||||
OwnsGateway {
|
||||
address: HumanAddr,
|
||||
},
|
||||
StateParams {},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// settings for pagination
|
||||
use crate::state::{gateways_read, mixnodes_read, PREFIX_MIXNODES};
|
||||
use crate::state::{config_read, gateways_read, mixnodes_read, StateParams, PREFIX_MIXNODES};
|
||||
use cosmwasm_std::Deps;
|
||||
use cosmwasm_std::HumanAddr;
|
||||
use cosmwasm_std::Order;
|
||||
@@ -90,10 +90,17 @@ fn calculate_start_value(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn query_state_params(deps: Deps) -> StateParams {
|
||||
// note: In any other case, I wouldn't have attempted to unwrap this result, but in here
|
||||
// if we fail to load the stored state we would already be in the undefined behaviour land,
|
||||
// so we better just blow up immediately.
|
||||
config_read(deps.storage).load().unwrap().params
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::{gateways, mixnodes};
|
||||
use crate::state::{config, gateways, mixnodes, State};
|
||||
use crate::support::tests::helpers;
|
||||
use cosmwasm_std::Storage;
|
||||
|
||||
@@ -389,4 +396,24 @@ mod tests {
|
||||
let res = query_owns_gateway(deps.as_ref(), "fred".into()).unwrap();
|
||||
assert!(!res.has_gateway);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_for_contract_state_works() {
|
||||
let mut deps = helpers::init_contract();
|
||||
|
||||
let dummy_state = State {
|
||||
owner: "someowner".into(),
|
||||
params: StateParams {
|
||||
minimum_mixnode_bond: 123u128.into(),
|
||||
minimum_gateway_bond: 456u128.into(),
|
||||
mixnode_bond_reward_rate: "1.23".parse().unwrap(),
|
||||
gateway_bond_reward_rate: "4.56".parse().unwrap(),
|
||||
mixnode_active_set_size: 1000,
|
||||
},
|
||||
};
|
||||
|
||||
config(deps.as_mut().storage).save(&dummy_state).unwrap();
|
||||
|
||||
assert_eq!(dummy_state.params, query_state_params(deps.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use cosmwasm_std::{HumanAddr, Storage};
|
||||
use cosmwasm_std::{Decimal, HumanAddr, Storage, Uint128};
|
||||
use cosmwasm_storage::{
|
||||
bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton,
|
||||
Singleton,
|
||||
@@ -13,7 +13,18 @@ pub static CONFIG_KEY: &[u8] = b"config";
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct State {
|
||||
pub owner: HumanAddr,
|
||||
pub owner: HumanAddr, // only the owner account can update state
|
||||
|
||||
pub params: StateParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct StateParams {
|
||||
pub minimum_mixnode_bond: Uint128, // minimum amount a mixnode must bond to get into the system
|
||||
pub minimum_gateway_bond: Uint128, // minimum amount a gateway must bond to get into the system
|
||||
pub mixnode_bond_reward_rate: Decimal, // epoch reward rate, expressed as a decimal like 1.25
|
||||
pub gateway_bond_reward_rate: Decimal, // epoch reward rate, expressed as a decimal like 1.25
|
||||
pub mixnode_active_set_size: u32,
|
||||
}
|
||||
|
||||
pub fn config(storage: &mut dyn Storage) -> Singleton<State> {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#[cfg(test)]
|
||||
pub mod helpers {
|
||||
use super::*;
|
||||
use crate::contract::init;
|
||||
use crate::contract::query;
|
||||
use crate::contract::try_add_mixnode;
|
||||
use crate::contract::{init, try_add_gateway};
|
||||
use crate::contract::DENOM;
|
||||
use crate::msg::InitMsg;
|
||||
use crate::msg::QueryMsg;
|
||||
use crate::transactions::{try_add_gateway, try_add_mixnode};
|
||||
use cosmwasm_std::coins;
|
||||
use cosmwasm_std::from_binary;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
@@ -22,8 +23,6 @@ pub mod helpers {
|
||||
Gateway, GatewayBond, MixNode, MixNodeBond, PagedGatewayResponse, PagedResponse,
|
||||
};
|
||||
|
||||
use crate::contract::DENOM;
|
||||
|
||||
pub fn add_mixnode(
|
||||
pubkey: &str,
|
||||
stake: Vec<Coin>,
|
||||
|
||||
@@ -0,0 +1,661 @@
|
||||
use crate::contract::DENOM;
|
||||
use crate::error::ContractError;
|
||||
use crate::queries::query_state_params;
|
||||
use crate::state::{
|
||||
config, config_read, gateways, gateways_read, mixnodes, mixnodes_read, StateParams,
|
||||
};
|
||||
use cosmwasm_std::{attr, BankMsg, Coin, DepsMut, Env, HandleResponse, MessageInfo, Uint128};
|
||||
use mixnet_contract::{Gateway, GatewayBond, MixNode, MixNodeBond};
|
||||
|
||||
fn validate_mixnode_bond(bond: &[Coin], minimum_bond: Uint128) -> Result<(), ContractError> {
|
||||
// check if anything was put as bond
|
||||
if bond.is_empty() {
|
||||
return Err(ContractError::NoBondFound);
|
||||
}
|
||||
|
||||
if bond.len() > 1 {
|
||||
// TODO: ask DH what would be an appropriate action here
|
||||
}
|
||||
|
||||
// check that the denomination is correct
|
||||
if bond[0].denom != DENOM {
|
||||
return Err(ContractError::WrongDenom {});
|
||||
}
|
||||
|
||||
// check that we have at least MIXNODE_BOND coins in our bond
|
||||
if bond[0].amount < minimum_bond {
|
||||
return Err(ContractError::InsufficientMixNodeBond {
|
||||
received: bond[0].amount.into(),
|
||||
minimum: minimum_bond.into(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn try_add_mixnode(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
mix_node: MixNode,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
let minimum_bond = query_state_params(deps.as_ref()).minimum_mixnode_bond;
|
||||
validate_mixnode_bond(&info.sent_funds, minimum_bond)?;
|
||||
|
||||
let bond = MixNodeBond::new(info.sent_funds, info.sender.clone(), mix_node);
|
||||
|
||||
let sender_bytes = info.sender.as_bytes();
|
||||
let was_present = mixnodes_read(deps.storage)
|
||||
.may_load(sender_bytes)?
|
||||
.is_some();
|
||||
|
||||
// TODO: do attributes also go back to the client or does this need to be put into `data`?
|
||||
let attributes = vec![attr("overwritten", was_present)];
|
||||
|
||||
mixnodes(deps.storage).save(sender_bytes, &bond)?;
|
||||
|
||||
Ok(HandleResponse {
|
||||
messages: vec![],
|
||||
attributes,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn try_remove_mixnode(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
env: Env,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
// find the bond, return ContractError::MixNodeBondNotFound if it doesn't exist
|
||||
let mixnode_bond = match mixnodes_read(deps.storage).may_load(info.sender.as_bytes())? {
|
||||
None => return Err(ContractError::MixNodeBondNotFound {}),
|
||||
Some(bond) => bond,
|
||||
};
|
||||
|
||||
// send bonded funds back to the bond owner
|
||||
let messages = vec![BankMsg::Send {
|
||||
from_address: env.contract.address,
|
||||
to_address: info.sender.clone(),
|
||||
amount: mixnode_bond.amount().to_vec(),
|
||||
}
|
||||
.into()];
|
||||
|
||||
// remove the bond from the list of bonded mixnodes
|
||||
mixnodes(deps.storage).remove(info.sender.as_bytes());
|
||||
|
||||
// log our actions
|
||||
let attributes = vec![attr("action", "unbond"), attr("mixnode_bond", mixnode_bond)];
|
||||
|
||||
Ok(HandleResponse {
|
||||
messages,
|
||||
attributes,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_gateway_bond(bond: &[Coin], minimum_bond: Uint128) -> Result<(), ContractError> {
|
||||
// check if anything was put as bond
|
||||
if bond.is_empty() {
|
||||
return Err(ContractError::NoBondFound);
|
||||
}
|
||||
|
||||
if bond.len() > 1 {
|
||||
// TODO: ask DH what would be an appropriate action here
|
||||
}
|
||||
|
||||
// check that the denomination is correct
|
||||
if bond[0].denom != DENOM {
|
||||
return Err(ContractError::WrongDenom {});
|
||||
}
|
||||
|
||||
// check that we have at least 100 coins in our bond
|
||||
if bond[0].amount < minimum_bond {
|
||||
return Err(ContractError::InsufficientGatewayBond {
|
||||
received: bond[0].amount.into(),
|
||||
minimum: minimum_bond.into(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn try_add_gateway(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
gateway: Gateway,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
let minimum_bond = query_state_params(deps.as_ref()).minimum_gateway_bond;
|
||||
validate_gateway_bond(&info.sent_funds, minimum_bond)?;
|
||||
|
||||
let bond = GatewayBond::new(info.sent_funds, info.sender.clone(), gateway);
|
||||
|
||||
let sender_bytes = info.sender.as_bytes();
|
||||
let was_present = gateways_read(deps.storage)
|
||||
.may_load(sender_bytes)?
|
||||
.is_some();
|
||||
|
||||
// TODO: do attributes also go back to the client or does this need to be put into `data`?
|
||||
let attributes = vec![attr("overwritten", was_present)];
|
||||
|
||||
gateways(deps.storage).save(sender_bytes, &bond)?;
|
||||
Ok(HandleResponse {
|
||||
messages: vec![],
|
||||
attributes,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn try_remove_gateway(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
env: Env,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
let sender_bytes = info.sender.as_bytes();
|
||||
|
||||
// find the bond, return ContractError::GatewayBondNotFound if it doesn't exist
|
||||
let gateway_bond = match gateways_read(deps.storage).may_load(sender_bytes)? {
|
||||
None => {
|
||||
return Err(ContractError::GatewayBondNotFound {
|
||||
account: info.sender,
|
||||
});
|
||||
}
|
||||
Some(bond) => bond,
|
||||
};
|
||||
|
||||
// send bonded funds back to the bond owner
|
||||
let messages = vec![BankMsg::Send {
|
||||
from_address: env.contract.address,
|
||||
to_address: info.sender.clone(),
|
||||
amount: gateway_bond.amount().to_vec(),
|
||||
}
|
||||
.into()];
|
||||
|
||||
// remove the bond from the list of bonded gateways
|
||||
gateways(deps.storage).remove(sender_bytes);
|
||||
|
||||
// log our actions
|
||||
let attributes = vec![
|
||||
attr("action", "unbond"),
|
||||
attr("address", info.sender),
|
||||
attr("gateway_bond", gateway_bond),
|
||||
];
|
||||
|
||||
Ok(HandleResponse {
|
||||
messages,
|
||||
attributes,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn try_update_state_params(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
params: StateParams,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
// note: In any other case, I wouldn't have attempted to unwrap this result, but in here
|
||||
// if we fail to load the stored state we would already be in the undefined behaviour land,
|
||||
// so we better just blow up immediately.
|
||||
let mut state = config_read(deps.storage).load().unwrap();
|
||||
|
||||
// check if this is executed by the owner, if not reject the transaction
|
||||
if info.sender != state.owner {
|
||||
return Err(ContractError::Unauthorized);
|
||||
}
|
||||
|
||||
state.params = params;
|
||||
config(deps.storage).save(&state)?;
|
||||
|
||||
Ok(HandleResponse::default())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::contract::{handle, init, query, INITIAL_GATEWAY_BOND, INITIAL_MIXNODE_BOND};
|
||||
use crate::msg::{HandleMsg, InitMsg, QueryMsg};
|
||||
use crate::support::tests::helpers;
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
|
||||
use cosmwasm_std::{coins, from_binary, Uint128};
|
||||
use mixnet_contract::{PagedGatewayResponse, PagedResponse};
|
||||
|
||||
fn good_mixnode_bond() -> Vec<Coin> {
|
||||
vec![Coin {
|
||||
denom: DENOM.to_string(),
|
||||
amount: INITIAL_MIXNODE_BOND,
|
||||
}]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validating_mixnode_bond() {
|
||||
// you must send SOME funds
|
||||
let result = validate_mixnode_bond(&[], INITIAL_MIXNODE_BOND);
|
||||
assert_eq!(result, Err(ContractError::NoBondFound));
|
||||
|
||||
// you must send at least 100 coins...
|
||||
let mut bond = good_mixnode_bond();
|
||||
bond[0].amount = (INITIAL_MIXNODE_BOND - Uint128(1)).unwrap();
|
||||
let result = validate_mixnode_bond(&bond, INITIAL_MIXNODE_BOND);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::InsufficientMixNodeBond {
|
||||
received: Into::<u128>::into(INITIAL_MIXNODE_BOND) - 1,
|
||||
minimum: INITIAL_MIXNODE_BOND.into(),
|
||||
})
|
||||
);
|
||||
|
||||
// more than that is still fine
|
||||
let mut bond = good_mixnode_bond();
|
||||
bond[0].amount = INITIAL_MIXNODE_BOND + Uint128(1);
|
||||
let result = validate_mixnode_bond(&bond, INITIAL_MIXNODE_BOND);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// it must be sent in the defined denom!
|
||||
let mut bond = good_mixnode_bond();
|
||||
bond[0].denom = "baddenom".to_string();
|
||||
let result = validate_mixnode_bond(&bond, INITIAL_MIXNODE_BOND);
|
||||
assert_eq!(result, Err(ContractError::WrongDenom {}));
|
||||
|
||||
let mut bond = good_mixnode_bond();
|
||||
bond[0].denom = "foomp".to_string();
|
||||
let result = validate_mixnode_bond(&bond, INITIAL_MIXNODE_BOND);
|
||||
assert_eq!(result, Err(ContractError::WrongDenom {}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnode_add() {
|
||||
let mut deps = helpers::init_contract();
|
||||
|
||||
// if we don't send enough funds
|
||||
let insufficient_bond = Into::<u128>::into(INITIAL_MIXNODE_BOND) - 1;
|
||||
let info = mock_info("anyone", &coins(insufficient_bond, DENOM));
|
||||
let msg = HandleMsg::BondMixnode {
|
||||
mix_node: helpers::mix_node_fixture(),
|
||||
};
|
||||
|
||||
// we are informed that we didn't send enough funds
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::InsufficientMixNodeBond {
|
||||
received: insufficient_bond,
|
||||
minimum: INITIAL_GATEWAY_BOND.into(),
|
||||
})
|
||||
);
|
||||
|
||||
// no mixnode was inserted into the topology
|
||||
let res = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedResponse = from_binary(&res).unwrap();
|
||||
assert_eq!(0, page.nodes.len());
|
||||
|
||||
// if we send enough funds
|
||||
let info = mock_info("anyone", &coins(1000_000000, DENOM));
|
||||
let msg = HandleMsg::BondMixnode {
|
||||
mix_node: helpers::mix_node_fixture(),
|
||||
};
|
||||
|
||||
// we get back a message telling us everything was OK
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert!(handle_response.is_ok());
|
||||
|
||||
// we can query topology and the new node is there
|
||||
let query_response = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedResponse = from_binary(&query_response).unwrap();
|
||||
assert_eq!(1, page.nodes.len());
|
||||
assert_eq!(&helpers::mix_node_fixture(), page.nodes[0].mix_node());
|
||||
|
||||
// if there was already a mixnode bonded by particular user
|
||||
let info = mock_info("foomper", &good_mixnode_bond());
|
||||
let msg = HandleMsg::BondGateway {
|
||||
gateway: helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(handle_response.attributes[0], attr("overwritten", false));
|
||||
|
||||
let info = mock_info("foomper", &good_mixnode_bond());
|
||||
let msg = HandleMsg::BondGateway {
|
||||
gateway: helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
// we get a log message about it (TODO: does it get back to the user?)
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(handle_response.attributes[0], attr("overwritten", true));
|
||||
|
||||
// adding another node from another account, but with the same IP, should fail (or we would have a weird state). Is that right? Think about this, not sure yet.
|
||||
// if we attempt to register a second node from the same address, should we get an error? It would probably be polite.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnode_remove() {
|
||||
let env = mock_env();
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let msg = InitMsg {};
|
||||
let info = mock_info("creator", &[]);
|
||||
init(deps.as_mut(), env.clone(), info, msg).unwrap();
|
||||
|
||||
// try un-registering when no nodes exist yet
|
||||
let info = mock_info("anyone", &coins(999_9999, DENOM));
|
||||
let msg = HandleMsg::UnbondMixnode {};
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
|
||||
// we're told that there is no node for our address
|
||||
assert_eq!(result, Err(ContractError::MixNodeBondNotFound {}));
|
||||
|
||||
// let's add a node owned by bob
|
||||
helpers::add_mixnode("bob", coins(1000_000000, DENOM), &mut deps);
|
||||
|
||||
// attempt to un-register fred's node, which doesn't exist
|
||||
let info = mock_info("fred", &coins(999_9999, DENOM));
|
||||
let msg = HandleMsg::UnbondMixnode {};
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(result, Err(ContractError::MixNodeBondNotFound {}));
|
||||
|
||||
// bob's node is still there
|
||||
let res = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedResponse = from_binary(&res).unwrap();
|
||||
let first_node = &page.nodes[0];
|
||||
assert_eq!(1, page.nodes.len());
|
||||
assert_eq!("bob", first_node.owner());
|
||||
|
||||
// add a node owned by fred
|
||||
let fred_bond = good_mixnode_bond();
|
||||
helpers::add_mixnode("fred", fred_bond.clone(), &mut deps);
|
||||
|
||||
// let's make sure we now have 2 nodes:
|
||||
assert_eq!(2, helpers::get_mix_nodes(&mut deps).len());
|
||||
|
||||
// un-register fred's node
|
||||
let info = mock_info("fred", &coins(999_9999, DENOM));
|
||||
let msg = HandleMsg::UnbondMixnode {};
|
||||
let remove_fred = handle(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
|
||||
|
||||
// we should see log messages come back showing an unbond message
|
||||
let expected_attributes = vec![
|
||||
attr("action", "unbond"),
|
||||
attr(
|
||||
"mixnode_bond",
|
||||
format!("amount: {} {}, owner: fred", INITIAL_MIXNODE_BOND, DENOM),
|
||||
),
|
||||
];
|
||||
|
||||
// we should see a funds transfer from the contract back to fred
|
||||
let expected_messages = vec![BankMsg::Send {
|
||||
from_address: env.contract.address,
|
||||
to_address: info.sender,
|
||||
amount: fred_bond,
|
||||
}
|
||||
.into()];
|
||||
|
||||
// run the handler and check that we got back the correct results
|
||||
let expected = HandleResponse {
|
||||
messages: expected_messages,
|
||||
attributes: expected_attributes,
|
||||
data: None,
|
||||
};
|
||||
assert_eq!(remove_fred, expected);
|
||||
|
||||
// only 1 node now exists, owned by bob:
|
||||
let mix_node_bonds = helpers::get_mix_nodes(&mut deps);
|
||||
assert_eq!(1, mix_node_bonds.len());
|
||||
assert_eq!("bob", mix_node_bonds[0].owner());
|
||||
}
|
||||
|
||||
fn good_gateway_bond() -> Vec<Coin> {
|
||||
vec![Coin {
|
||||
denom: DENOM.to_string(),
|
||||
amount: INITIAL_GATEWAY_BOND,
|
||||
}]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validating_gateway_bond() {
|
||||
// you must send SOME funds
|
||||
let result = validate_gateway_bond(&[], INITIAL_GATEWAY_BOND);
|
||||
assert_eq!(result, Err(ContractError::NoBondFound));
|
||||
|
||||
// you must send at least 100 coins...
|
||||
let mut bond = good_gateway_bond();
|
||||
bond[0].amount = (INITIAL_GATEWAY_BOND - Uint128(1)).unwrap();
|
||||
let result = validate_gateway_bond(&bond, INITIAL_GATEWAY_BOND);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::InsufficientGatewayBond {
|
||||
received: Into::<u128>::into(INITIAL_GATEWAY_BOND) - 1,
|
||||
minimum: INITIAL_GATEWAY_BOND.into(),
|
||||
})
|
||||
);
|
||||
|
||||
// more than that is still fine
|
||||
let mut bond = good_gateway_bond();
|
||||
bond[0].amount = INITIAL_GATEWAY_BOND + Uint128(1);
|
||||
let result = validate_gateway_bond(&bond, INITIAL_GATEWAY_BOND);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// it must be sent in the defined denom!
|
||||
let mut bond = good_gateway_bond();
|
||||
bond[0].denom = "baddenom".to_string();
|
||||
let result = validate_gateway_bond(&bond, INITIAL_GATEWAY_BOND);
|
||||
assert_eq!(result, Err(ContractError::WrongDenom {}));
|
||||
|
||||
let mut bond = good_gateway_bond();
|
||||
bond[0].denom = "foomp".to_string();
|
||||
let result = validate_gateway_bond(&bond, INITIAL_GATEWAY_BOND);
|
||||
assert_eq!(result, Err(ContractError::WrongDenom {}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_add() {
|
||||
let mut deps = helpers::init_contract();
|
||||
|
||||
// if we fail validation (by say not sending enough funds
|
||||
let insufficient_bond = Into::<u128>::into(INITIAL_GATEWAY_BOND) - 1;
|
||||
let info = mock_info("anyone", &coins(insufficient_bond, DENOM));
|
||||
let msg = HandleMsg::BondGateway {
|
||||
gateway: helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
// we are informed that we didn't send enough funds
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::InsufficientGatewayBond {
|
||||
received: insufficient_bond,
|
||||
minimum: INITIAL_GATEWAY_BOND.into(),
|
||||
})
|
||||
);
|
||||
|
||||
// make sure no gateway was inserted into the topology
|
||||
let res = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetGateways {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedGatewayResponse = from_binary(&res).unwrap();
|
||||
assert_eq!(0, page.nodes.len());
|
||||
|
||||
// if we send enough funds
|
||||
let info = mock_info("anyone", &good_gateway_bond());
|
||||
let msg = HandleMsg::BondGateway {
|
||||
gateway: helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
// we get back a message telling us everything was OK
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert!(handle_response.is_ok());
|
||||
|
||||
// we can query topology and the new node is there
|
||||
let query_response = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetGateways {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedGatewayResponse = from_binary(&query_response).unwrap();
|
||||
assert_eq!(1, page.nodes.len());
|
||||
assert_eq!(&helpers::gateway_fixture(), page.nodes[0].gateway());
|
||||
|
||||
// if there was already a gateway bonded by particular user
|
||||
let info = mock_info("foomper", &good_gateway_bond());
|
||||
let msg = HandleMsg::BondGateway {
|
||||
gateway: helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(handle_response.attributes[0], attr("overwritten", false));
|
||||
|
||||
let info = mock_info("foomper", &good_gateway_bond());
|
||||
let msg = HandleMsg::BondGateway {
|
||||
gateway: helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
// we get a log message about it (TODO: does it get back to the user?)
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(handle_response.attributes[0], attr("overwritten", true));
|
||||
|
||||
// adding another node from another account, but with the same IP, should fail (or we would have a weird state).
|
||||
// Is that right? Think about this, not sure yet.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_remove() {
|
||||
let env = mock_env();
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let msg = InitMsg {};
|
||||
let info = mock_info("creator", &[]);
|
||||
init(deps.as_mut(), env.clone(), info, msg).unwrap();
|
||||
|
||||
// try unbond when no nodes exist yet
|
||||
let info = mock_info("anyone", &[]);
|
||||
let msg = HandleMsg::UnbondGateway {};
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
|
||||
// we're told that there is no node for our address
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::GatewayBondNotFound {
|
||||
account: "anyone".into()
|
||||
})
|
||||
);
|
||||
|
||||
// let's add a node owned by bob
|
||||
helpers::add_gateway("bob", good_gateway_bond(), &mut deps);
|
||||
|
||||
// attempt to unbond fred's node, which doesn't exist
|
||||
let info = mock_info("fred", &[]);
|
||||
let msg = HandleMsg::UnbondGateway {};
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::GatewayBondNotFound {
|
||||
account: "fred".into()
|
||||
})
|
||||
);
|
||||
|
||||
// bob's node is still there
|
||||
let nodes = helpers::get_gateways(&mut deps);
|
||||
assert_eq!(1, nodes.len());
|
||||
|
||||
let first_node = &nodes[0];
|
||||
assert_eq!("bob", first_node.owner());
|
||||
|
||||
// add a node owned by fred
|
||||
let fred_bond = good_gateway_bond();
|
||||
helpers::add_gateway("fred", fred_bond.clone(), &mut deps);
|
||||
|
||||
// let's make sure we now have 2 nodes:
|
||||
assert_eq!(2, helpers::get_gateways(&mut deps).len());
|
||||
|
||||
// unbond fred's node
|
||||
let info = mock_info("fred", &[]);
|
||||
let msg = HandleMsg::UnbondGateway {};
|
||||
let remove_fred = handle(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
|
||||
|
||||
// we should see log messages come back showing an unbond message
|
||||
let expected_attributes = vec![
|
||||
attr("action", "unbond"),
|
||||
attr("address", "fred"),
|
||||
attr(
|
||||
"gateway_bond",
|
||||
format!("amount: {} {}, owner: fred", INITIAL_GATEWAY_BOND, DENOM),
|
||||
),
|
||||
];
|
||||
|
||||
// we should see a funds transfer from the contract back to fred
|
||||
let expected_messages = vec![BankMsg::Send {
|
||||
from_address: env.contract.address,
|
||||
to_address: info.sender,
|
||||
amount: fred_bond,
|
||||
}
|
||||
.into()];
|
||||
|
||||
// run the handler and check that we got back the correct results
|
||||
let expected = HandleResponse {
|
||||
messages: expected_messages,
|
||||
attributes: expected_attributes,
|
||||
data: None,
|
||||
};
|
||||
assert_eq!(remove_fred, expected);
|
||||
|
||||
// only 1 node now exists, owned by bob:
|
||||
let gateway_bonds = helpers::get_gateways(&mut deps);
|
||||
assert_eq!(1, gateway_bonds.len());
|
||||
assert_eq!("bob", gateway_bonds[0].owner());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updating_state_params() {
|
||||
let mut deps = helpers::init_contract();
|
||||
|
||||
let new_params = StateParams {
|
||||
minimum_mixnode_bond: 123u128.into(),
|
||||
minimum_gateway_bond: 456u128.into(),
|
||||
mixnode_bond_reward_rate: "1.23".parse().unwrap(),
|
||||
gateway_bond_reward_rate: "4.56".parse().unwrap(),
|
||||
mixnode_active_set_size: 1000,
|
||||
};
|
||||
|
||||
// cannot be updated from non-owner account
|
||||
let info = mock_info("not-the-creator", &[]);
|
||||
let res = try_update_state_params(deps.as_mut(), info, new_params.clone());
|
||||
assert_eq!(res, Err(ContractError::Unauthorized));
|
||||
|
||||
// but works fine from the creator account
|
||||
let info = mock_info("creator", &[]);
|
||||
let res = try_update_state_params(deps.as_mut(), info, new_params.clone());
|
||||
assert_eq!(res, Ok(HandleResponse::default()));
|
||||
|
||||
// and the state is actually updated
|
||||
let current_state = config_read(deps.as_ref().storage).load().unwrap();
|
||||
assert_eq!(current_state.params, new_params)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user