use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use snafu::{OptionExt, ResultExt};
use cosmwasm::errors::{ContractErr, ParseErr, Result, SerializeErr, Unauthorized};
use cosmwasm::serde::{from_slice, to_vec};
use cosmwasm::traits::{Api, Extern, Storage};
use cosmwasm::types::{CanonicalAddr, Coin, CosmosMsg, HumanAddr, Params, Response};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InitMsg {
pub arbiter: HumanAddr,
pub recipient: HumanAddr,
pub end_height: i64,
pub end_time: i64,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum HandleMsg {
Approve {
quantity: Option<Vec<Coin>>,
},
Refund {},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum QueryMsg {
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct State {
pub arbiter: CanonicalAddr,
pub recipient: CanonicalAddr,
pub source: CanonicalAddr,
pub end_height: i64,
pub end_time: i64,
}
impl State {
fn is_expired(&self, params: &Params) -> bool {
(self.end_height != 0 && params.block.height >= self.end_height)
|| (self.end_time != 0 && params.block.time >= self.end_time)
}
}
pub static CONFIG_KEY: &[u8] = b"config";
pub fn init<S: Storage, A: Api>(
deps: &mut Extern<S, A>,
params: Params,
msg: InitMsg,
) -> Result<Response> {
let state = State {
arbiter: deps.api.canonical_address(&msg.arbiter)?,
recipient: deps.api.canonical_address(&msg.recipient)?,
source: params.message.signer.clone(),
end_height: msg.end_height,
end_time: msg.end_time,
};
if state.is_expired(¶ms) {
ContractErr {
msg: "creating expired escrow",
}
.fail()
} else {
deps.storage.set(
CONFIG_KEY,
&to_vec(&state).context(SerializeErr { kind: "State" })?,
);
Ok(Response::default())
}
}
pub fn handle<S: Storage, A: Api>(
deps: &mut Extern<S, A>,
params: Params,
msg: HandleMsg,
) -> Result<Response> {
let data = deps.storage.get(CONFIG_KEY).context(ContractErr {
msg: "uninitialized data",
})?;
let state: State = from_slice(&data).context(ParseErr { kind: "State" })?;
match msg {
HandleMsg::Approve { quantity } => try_approve(&deps.api, params, state, quantity),
HandleMsg::Refund {} => try_refund(&deps.api, params, state),
}
}
fn try_approve<A: Api>(
api: &A,
params: Params,
state: State,
quantity: Option<Vec<Coin>>,
) -> Result<Response> {
if params.message.signer != state.arbiter {
Unauthorized {}.fail()
} else if state.is_expired(¶ms) {
ContractErr {
msg: "escrow expired",
}
.fail()
} else {
let amount = match quantity {
None => params.contract.balance.unwrap_or_default(),
Some(coins) => coins,
};
let res = Response {
messages: vec![CosmosMsg::Send {
from_address: api.human_address(¶ms.contract.address)?,
to_address: api.human_address(&state.recipient)?,
amount,
}],
log: Some("paid out funds".to_string()),
data: None,
};
Ok(res)
}
}
fn try_refund<A: Api>(api: &A, params: Params, state: State) -> Result<Response> {
if !state.is_expired(¶ms) {
ContractErr {
msg: "escrow not yet expired",
}
.fail()
} else {
let res = Response {
messages: vec![CosmosMsg::Send {
from_address: api.human_address(¶ms.contract.address)?,
to_address: api.human_address(&state.source)?,
amount: params.contract.balance.unwrap_or_default(),
}],
log: Some("returned funds".to_string()),
data: None,
};
Ok(res)
}
}
pub fn query<S: Storage, A: Api>(_deps: &Extern<S, A>, msg: QueryMsg) -> Result<Vec<u8>> {
match msg {
}
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm::errors::Error;
use cosmwasm::mock::{dependencies, mock_params};
use cosmwasm::traits::{Api, ReadonlyStorage};
use cosmwasm::types::coin;
fn init_msg(height: i64, time: i64) -> InitMsg {
InitMsg {
arbiter: HumanAddr::from("verifies"),
recipient: HumanAddr::from("benefits"),
end_height: height,
end_time: time,
}
}
fn mock_params_height<A: Api>(
api: &A,
signer: &str,
sent: &[Coin],
balance: &[Coin],
height: i64,
time: i64,
) -> Params {
let mut params = mock_params(api, signer, sent, balance);
params.block.height = height;
params.block.time = time;
params
}
#[test]
fn proper_initialization() {
let mut deps = dependencies(20);
let msg = init_msg(1000, 0);
let params = mock_params_height(&deps.api, "creator", &coin("1000", "earth"), &[], 876, 0);
let res = init(&mut deps, params, msg).unwrap();
assert_eq!(0, res.messages.len());
let val = deps.storage.get(CONFIG_KEY).expect("init must set data");
let state: State = from_slice(&val).unwrap();
assert_eq!(
state,
State {
arbiter: deps
.api
.canonical_address(&HumanAddr::from("verifies"))
.unwrap(),
recipient: deps
.api
.canonical_address(&HumanAddr::from("benefits"))
.unwrap(),
source: deps
.api
.canonical_address(&HumanAddr::from("creator"))
.unwrap(),
end_height: 1000,
end_time: 0,
}
);
}
#[test]
fn cannot_initialize_expired() {
let mut deps = dependencies(20);
let msg = init_msg(1000, 0);
let params = mock_params_height(&deps.api, "creator", &coin("1000", "earth"), &[], 1001, 0);
let res = init(&mut deps, params, msg);
assert!(res.is_err());
if let Err(Error::ContractErr { msg, .. }) = res {
assert_eq!(msg, "creating expired escrow".to_string());
} else {
assert!(false, "wrong error type");
}
}
#[test]
fn handle_approve() {
let mut deps = dependencies(20);
let msg = init_msg(1000, 0);
let params = mock_params_height(&deps.api, "creator", &coin("1000", "earth"), &[], 876, 0);
let init_res = init(&mut deps, params, msg).unwrap();
assert_eq!(0, init_res.messages.len());
let msg = HandleMsg::Approve { quantity: None };
let params = mock_params_height(
&deps.api,
"beneficiary",
&coin("0", "earth"),
&coin("1000", "earth"),
900,
0,
);
let handle_res = handle(&mut deps, params, msg.clone());
match handle_res {
Ok(_) => panic!("expected error"),
Err(Error::Unauthorized { .. }) => {}
Err(e) => panic!("unexpected error: {:?}", e),
}
let params = mock_params_height(
&deps.api,
"verifies",
&coin("0", "earth"),
&coin("1000", "earth"),
1100,
0,
);
let handle_res = handle(&mut deps, params, msg.clone());
match handle_res {
Ok(_) => panic!("expected error"),
Err(Error::ContractErr { msg, .. }) => assert_eq!(msg, "escrow expired".to_string()),
Err(e) => panic!("unexpected error: {:?}", e),
}
let params = mock_params_height(
&deps.api,
"verifies",
&coin("0", "earth"),
&coin("1000", "earth"),
999,
0,
);
let handle_res = handle(&mut deps, params, msg.clone()).unwrap();
assert_eq!(1, handle_res.messages.len());
let msg = handle_res.messages.get(0).expect("no message");
assert_eq!(
msg,
&CosmosMsg::Send {
from_address: HumanAddr::from("cosmos2contract"),
to_address: HumanAddr::from("benefits"),
amount: coin("1000", "earth"),
}
);
let partial_msg = HandleMsg::Approve {
quantity: Some(coin("500", "earth")),
};
let params = mock_params_height(
&deps.api,
"verifies",
&coin("0", "earth"),
&coin("1000", "earth"),
999,
0,
);
let handle_res = handle(&mut deps, params, partial_msg).unwrap();
assert_eq!(1, handle_res.messages.len());
let msg = handle_res.messages.get(0).expect("no message");
assert_eq!(
msg,
&CosmosMsg::Send {
from_address: HumanAddr::from("cosmos2contract"),
to_address: HumanAddr::from("benefits"),
amount: coin("500", "earth"),
}
);
}
#[test]
fn handle_refund() {
let mut deps = dependencies(20);
let msg = init_msg(1000, 0);
let params = mock_params_height(&deps.api, "creator", &coin("1000", "earth"), &[], 876, 0);
let init_res = init(&mut deps, params, msg).unwrap();
assert_eq!(0, init_res.messages.len());
let msg = HandleMsg::Refund {};
let params = mock_params_height(
&deps.api,
"anybody",
&coin("0", "earth"),
&coin("1000", "earth"),
800,
0,
);
let handle_res = handle(&mut deps, params, msg.clone());
match handle_res {
Ok(_) => panic!("expected error"),
Err(Error::ContractErr { msg, .. }) => {
assert_eq!(msg, "escrow not yet expired".to_string())
}
Err(e) => panic!("unexpected error: {:?}", e),
}
let params = mock_params_height(
&deps.api,
"anybody",
&coin("0", "earth"),
&coin("1000", "earth"),
1001,
0,
);
let handle_res = handle(&mut deps, params, msg.clone()).unwrap();
assert_eq!(1, handle_res.messages.len());
let msg = handle_res.messages.get(0).expect("no message");
assert_eq!(
msg,
&CosmosMsg::Send {
from_address: HumanAddr::from("cosmos2contract"),
to_address: HumanAddr::from("creator"),
amount: coin("1000", "earth"),
}
);
}
}