github.com/fibonacci-chain/fbc@v0.0.0-20231124064014-c7636198c1e9/dev/wasm/escrow/src/contract.rs (about)

     1  use cosmwasm_std::{
     2      entry_point, to_binary, Addr, BankMsg, Binary, Coin, Deps, DepsMut, Env, MessageInfo, Response,
     3      StdResult,
     4  };
     5  
     6  use crate::error::ContractError;
     7  use crate::msg::{ArbiterResponse, ExecuteMsg, InstantiateMsg, QueryMsg};
     8  use crate::state::{config, config_read, State};
     9  
    10  #[entry_point]
    11  pub fn instantiate(
    12      deps: DepsMut,
    13      env: Env,
    14      info: MessageInfo,
    15      msg: InstantiateMsg,
    16  ) -> Result<Response, ContractError> {
    17      let state = State {
    18          arbiter: deps.api.addr_validate(&msg.arbiter)?,
    19          recipient: deps.api.addr_validate(&msg.recipient)?,
    20          source: info.sender,
    21          end_height: msg.end_height,
    22          end_time: msg.end_time,
    23      };
    24  
    25      if state.is_expired(&env) {
    26          return Err(ContractError::Expired {
    27              end_height: msg.end_height,
    28              end_time: msg.end_time,
    29          });
    30      }
    31  
    32      config(deps.storage).save(&state)?;
    33      Ok(Response::default())
    34  }
    35  
    36  #[entry_point]
    37  pub fn execute(
    38      deps: DepsMut,
    39      env: Env,
    40      info: MessageInfo,
    41      msg: ExecuteMsg,
    42  ) -> Result<Response, ContractError> {
    43      let state = config_read(deps.storage).load()?;
    44      match msg {
    45          ExecuteMsg::Approve { quantity } => try_approve(deps, env, state, info, quantity),
    46          ExecuteMsg::Refund {} => try_refund(deps, env, info, state),
    47      }
    48  }
    49  
    50  fn try_approve(
    51      deps: DepsMut,
    52      env: Env,
    53      state: State,
    54      info: MessageInfo,
    55      quantity: Option<Vec<Coin>>,
    56  ) -> Result<Response, ContractError> {
    57      if info.sender != state.arbiter {
    58          return Err(ContractError::Unauthorized {});
    59      }
    60  
    61      // throws error if state is expired
    62      if state.is_expired(&env) {
    63          return Err(ContractError::Expired {
    64              end_height: state.end_height,
    65              end_time: state.end_time,
    66          });
    67      }
    68  
    69      let amount = if let Some(quantity) = quantity {
    70          quantity
    71      } else {
    72          // release everything
    73  
    74          // Querier guarantees to returns up-to-date data, including funds sent in this handle message
    75          // https://github.com/CosmWasm/wasmd/blob/master/x/wasm/internal/keeper/keeper.go#L185-L192
    76          deps.querier.query_all_balances(&env.contract.address)?
    77      };
    78  
    79      Ok(send_tokens(state.recipient, amount, "approve"))
    80  }
    81  
    82  fn try_refund(
    83      deps: DepsMut,
    84      env: Env,
    85      _info: MessageInfo,
    86      state: State,
    87  ) -> Result<Response, ContractError> {
    88      // anyone can try to refund, as long as the contract is expired
    89      if !state.is_expired(&env) {
    90          return Err(ContractError::NotExpired {});
    91      }
    92  
    93      // Querier guarantees to returns up-to-date data, including funds sent in this handle message
    94      // https://github.com/CosmWasm/wasmd/blob/master/x/wasm/internal/keeper/keeper.go#L185-L192
    95      let balance = deps.querier.query_all_balances(&env.contract.address)?;
    96      Ok(send_tokens(state.source, balance, "refund"))
    97  }
    98  
    99  // this is a helper to move the tokens, so the business logic is easy to read
   100  fn send_tokens(to_address: Addr, amount: Vec<Coin>, action: &str) -> Response {
   101      Response::new()
   102          .add_message(BankMsg::Send {
   103              to_address: to_address.clone().into(),
   104              amount,
   105          })
   106          .add_attribute("action", action)
   107          .add_attribute("to", to_address)
   108  }
   109  
   110  #[entry_point]
   111  pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
   112      match msg {
   113          QueryMsg::Arbiter {} => to_binary(&query_arbiter(deps)?),
   114      }
   115  }
   116  
   117  fn query_arbiter(deps: Deps) -> StdResult<ArbiterResponse> {
   118      let state = config_read(deps.storage).load()?;
   119      let addr = state.arbiter;
   120      Ok(ArbiterResponse { arbiter: addr })
   121  }
   122  
   123  #[cfg(test)]
   124  mod tests {
   125      use super::*;
   126      use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
   127      use cosmwasm_std::{coins, CosmosMsg, Timestamp};
   128  
   129      fn init_msg_expire_by_height(height: u64) -> InstantiateMsg {
   130          InstantiateMsg {
   131              arbiter: String::from("verifies"),
   132              recipient: String::from("benefits"),
   133              end_height: Some(height),
   134              end_time: None,
   135          }
   136      }
   137  
   138      #[test]
   139      fn proper_initialization() {
   140          let mut deps = mock_dependencies(&[]);
   141  
   142          let msg = init_msg_expire_by_height(1000);
   143          let mut env = mock_env();
   144          env.block.height = 876;
   145          env.block.time = Timestamp::from_seconds(0);
   146          let info = mock_info("creator", &coins(1000, "earth"));
   147  
   148          let res = instantiate(deps.as_mut(), env, info, msg).unwrap();
   149          assert_eq!(0, res.messages.len());
   150  
   151          // it worked, let's query the state
   152          let state = config_read(&mut deps.storage).load().unwrap();
   153          assert_eq!(
   154              state,
   155              State {
   156                  arbiter: Addr::unchecked("verifies"),
   157                  recipient: Addr::unchecked("benefits"),
   158                  source: Addr::unchecked("creator"),
   159                  end_height: Some(1000),
   160                  end_time: None,
   161              }
   162          );
   163      }
   164  
   165      #[test]
   166      fn cannot_initialize_expired() {
   167          let mut deps = mock_dependencies(&[]);
   168  
   169          let msg = init_msg_expire_by_height(1000);
   170          let mut env = mock_env();
   171          env.block.height = 1001;
   172          env.block.time = Timestamp::from_seconds(0);
   173          let info = mock_info("creator", &coins(1000, "earth"));
   174  
   175          let res = instantiate(deps.as_mut(), env, info, msg);
   176          match res.unwrap_err() {
   177              ContractError::Expired { .. } => {}
   178              e => panic!("unexpected error: {:?}", e),
   179          }
   180      }
   181  
   182      #[test]
   183      fn init_and_query() {
   184          let mut deps = mock_dependencies(&[]);
   185  
   186          let arbiter = Addr::unchecked("arbiters");
   187          let recipient = Addr::unchecked("receives");
   188          let creator = Addr::unchecked("creates");
   189          let msg = InstantiateMsg {
   190              arbiter: arbiter.clone().into(),
   191              recipient: recipient.into(),
   192              end_height: None,
   193              end_time: None,
   194          };
   195          let mut env = mock_env();
   196          env.block.height = 876;
   197          env.block.time = Timestamp::from_seconds(0);
   198          let info = mock_info(creator.as_str(), &[]);
   199          let res = instantiate(deps.as_mut(), env, info, msg).unwrap();
   200          assert_eq!(0, res.messages.len());
   201  
   202          // now let's query
   203          let query_response = query_arbiter(deps.as_ref()).unwrap();
   204          assert_eq!(query_response.arbiter, arbiter);
   205      }
   206  
   207      #[test]
   208      fn execute_approve() {
   209          let mut deps = mock_dependencies(&[]);
   210  
   211          // initialize the store
   212          let init_amount = coins(1000, "earth");
   213          let msg = init_msg_expire_by_height(1000);
   214          let mut env = mock_env();
   215          env.block.height = 876;
   216          env.block.time = Timestamp::from_seconds(0);
   217          let info = mock_info("creator", &init_amount);
   218          let contract_addr = env.clone().contract.address;
   219          let init_res = instantiate(deps.as_mut(), env, info, msg).unwrap();
   220          assert_eq!(0, init_res.messages.len());
   221  
   222          // balance changed in init
   223          deps.querier.update_balance(&contract_addr, init_amount);
   224  
   225          // beneficiary cannot release it
   226          let msg = ExecuteMsg::Approve { quantity: None };
   227          let mut env = mock_env();
   228          env.block.height = 900;
   229          env.block.time = Timestamp::from_seconds(0);
   230          let info = mock_info("beneficiary", &[]);
   231          let execute_res = execute(deps.as_mut(), env, info, msg.clone());
   232          match execute_res.unwrap_err() {
   233              ContractError::Unauthorized { .. } => {}
   234              e => panic!("unexpected error: {:?}", e),
   235          }
   236  
   237          // verifier cannot release it when expired
   238          let mut env = mock_env();
   239          env.block.height = 1100;
   240          env.block.time = Timestamp::from_seconds(0);
   241          let info = mock_info("verifies", &[]);
   242          let execute_res = execute(deps.as_mut(), env, info, msg.clone());
   243          match execute_res.unwrap_err() {
   244              ContractError::Expired { .. } => {}
   245              e => panic!("unexpected error: {:?}", e),
   246          }
   247  
   248          // complete release by verfier, before expiration
   249          let mut env = mock_env();
   250          env.block.height = 999;
   251          env.block.time = Timestamp::from_seconds(0);
   252          let info = mock_info("verifies", &[]);
   253          let execute_res = execute(deps.as_mut(), env, info, msg.clone()).unwrap();
   254          assert_eq!(1, execute_res.messages.len());
   255          let msg = execute_res.messages.get(0).expect("no message");
   256          assert_eq!(
   257              msg.msg,
   258              CosmosMsg::Bank(BankMsg::Send {
   259                  to_address: "benefits".into(),
   260                  amount: coins(1000, "earth"),
   261              })
   262          );
   263  
   264          // partial release by verfier, before expiration
   265          let partial_msg = ExecuteMsg::Approve {
   266              quantity: Some(coins(500, "earth")),
   267          };
   268          let mut env = mock_env();
   269          env.block.height = 999;
   270          env.block.time = Timestamp::from_seconds(0);
   271          let info = mock_info("verifies", &[]);
   272          let execute_res = execute(deps.as_mut(), env, info, partial_msg).unwrap();
   273          assert_eq!(1, execute_res.messages.len());
   274          let msg = execute_res.messages.get(0).expect("no message");
   275          assert_eq!(
   276              msg.msg,
   277              CosmosMsg::Bank(BankMsg::Send {
   278                  to_address: "benefits".into(),
   279                  amount: coins(500, "earth"),
   280              })
   281          );
   282      }
   283  
   284      #[test]
   285      fn handle_refund() {
   286          let mut deps = mock_dependencies(&[]);
   287  
   288          // initialize the store
   289          let init_amount = coins(1000, "earth");
   290          let msg = init_msg_expire_by_height(1000);
   291          let mut env = mock_env();
   292          env.block.height = 876;
   293          env.block.time = Timestamp::from_seconds(0);
   294          let info = mock_info("creator", &init_amount);
   295          let contract_addr = env.clone().contract.address;
   296          let init_res = instantiate(deps.as_mut(), env, info, msg).unwrap();
   297          assert_eq!(0, init_res.messages.len());
   298  
   299          // balance changed in init
   300          deps.querier.update_balance(&contract_addr, init_amount);
   301  
   302          // cannot release when unexpired (height < end_height)
   303          let msg = ExecuteMsg::Refund {};
   304          let mut env = mock_env();
   305          env.block.height = 800;
   306          env.block.time = Timestamp::from_seconds(0);
   307          let info = mock_info("anybody", &[]);
   308          let execute_res = execute(deps.as_mut(), env, info, msg.clone());
   309          match execute_res.unwrap_err() {
   310              ContractError::NotExpired { .. } => {}
   311              e => panic!("unexpected error: {:?}", e),
   312          }
   313  
   314          // cannot release when unexpired (height == end_height)
   315          let msg = ExecuteMsg::Refund {};
   316          let mut env = mock_env();
   317          env.block.height = 1000;
   318          env.block.time = Timestamp::from_seconds(0);
   319          let info = mock_info("anybody", &[]);
   320          let execute_res = execute(deps.as_mut(), env, info, msg.clone());
   321          match execute_res.unwrap_err() {
   322              ContractError::NotExpired { .. } => {}
   323              e => panic!("unexpected error: {:?}", e),
   324          }
   325  
   326          // anyone can release after expiration
   327          let mut env = mock_env();
   328          env.block.height = 1001;
   329          env.block.time = Timestamp::from_seconds(0);
   330          let info = mock_info("anybody", &[]);
   331          let execute_res = execute(deps.as_mut(), env, info, msg.clone()).unwrap();
   332          assert_eq!(1, execute_res.messages.len());
   333          let msg = execute_res.messages.get(0).expect("no message");
   334          assert_eq!(
   335              msg.msg,
   336              CosmosMsg::Bank(BankMsg::Send {
   337                  to_address: "creator".into(),
   338                  amount: coins(1000, "earth"),
   339              })
   340          );
   341      }
   342  }