1use cosmwasm_std::{
2 coin, entry_point, to_binary, BankMsg, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env,
3 MessageInfo, Response, StdError, StdResult,
4};
5use provwasm_std::{bind_name, NameBinding, ProvenanceMsg, ProvenanceQuery};
6use std::ops::Mul;
7
8use crate::error::ContractError;
9use crate::msg::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg};
10use crate::state::{config, config_read, State};
11
12#[entry_point]
14pub fn instantiate(
15 deps: DepsMut<ProvenanceQuery>,
16 env: Env,
17 info: MessageInfo,
18 msg: InitMsg,
19) -> Result<Response<ProvenanceMsg>, StdError> {
20 if !info.funds.is_empty() {
22 let err = "purchase funds are not allowed to be sent during init";
23 return Err(StdError::generic_err(err));
24 }
25
26 if msg.fee_percent.is_zero() || msg.fee_percent > Decimal::percent(25) {
28 return Err(StdError::generic_err(
29 "fee percent must be > 0.0 and <= 0.25",
30 ));
31 }
32
33 if msg.merchant_address == info.sender {
35 return Err(StdError::generic_err(
36 "merchant address can't be the fee collection address",
37 ));
38 }
39
40 let merchant_address = deps.api.addr_validate(&msg.merchant_address)?;
43 config(deps.storage).save(&State {
44 purchase_denom: msg.purchase_denom,
45 merchant_address,
46 fee_collection_address: info.sender,
47 fee_percent: msg.fee_percent,
48 })?;
49
50 let msg = bind_name(
52 &msg.contract_name,
53 env.contract.address,
54 NameBinding::Restricted,
55 )?;
56
57 Ok(Response::new()
58 .add_message(msg)
59 .add_attribute("action", "init"))
60}
61
62#[entry_point]
64pub fn query(
65 deps: Deps<ProvenanceQuery>,
66 _env: Env, msg: QueryMsg,
68) -> StdResult<Binary> {
69 match msg {
70 QueryMsg::QueryRequest {} => {
71 let state = config_read(deps.storage).load()?;
72 let json = to_binary(&state)?;
73 Ok(json)
74 }
75 }
76}
77
78#[entry_point]
80pub fn execute(
81 deps: DepsMut<ProvenanceQuery>,
82 env: Env,
83 info: MessageInfo,
84 msg: ExecuteMsg,
85) -> Result<Response<ProvenanceMsg>, ContractError> {
86 match msg {
88 ExecuteMsg::Purchase { id } => try_purchase(deps, env, info, id),
89 }
90}
91
92#[entry_point]
94pub fn migrate(
95 _deps: DepsMut<ProvenanceQuery>,
96 _env: Env,
97 _msg: MigrateMsg,
98) -> Result<Response, ContractError> {
99 Ok(Response::default())
100}
101
102fn try_purchase(
104 deps: DepsMut<ProvenanceQuery>,
105 env: Env,
106 info: MessageInfo,
107 id: String,
108) -> Result<Response<ProvenanceMsg>, ContractError> {
109 if info.funds.is_empty() {
111 let err = "no purchase funds sent";
112 return Err(ContractError::Std(StdError::generic_err(err)));
113 }
114
115 let state = config_read(deps.storage).load()?;
117 let fee_pct = state.fee_percent;
118
119 for funds in info.funds.iter() {
121 if funds.amount.is_zero() || funds.denom != state.purchase_denom {
122 let err = format!("invalid purchase funds: {}{}", funds.amount, funds.denom);
123 return Err(ContractError::Std(StdError::generic_err(err)));
124 }
125 }
126
127 let transfers = CosmosMsg::Bank(BankMsg::Send {
129 to_address: state.merchant_address.to_string(),
130 amount: info
131 .funds
132 .iter()
133 .map(|sent| {
134 let fees = sent.amount.mul(fee_pct).u128();
135 coin(sent.amount.u128() - fees, sent.denom.clone())
136 })
137 .collect(),
138 });
139
140 let fees = CosmosMsg::Bank(BankMsg::Send {
142 to_address: state.fee_collection_address.to_string(),
143 amount: info
144 .funds
145 .iter()
146 .map(|sent| coin(sent.amount.mul(fee_pct).u128(), sent.denom.clone()))
147 .collect(),
148 });
149
150 Ok(Response::new()
152 .add_message(transfers)
153 .add_message(fees)
154 .add_attribute("action", "purchase")
155 .add_attribute("purchase_id", id)
156 .add_attribute("purchase_time", env.block.time.to_string()))
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::msg::QueryResponse;
163 use cosmwasm_std::testing::{mock_env, mock_info};
164 use cosmwasm_std::{from_binary, Addr};
165 use provwasm_mocks::mock_dependencies;
166 use provwasm_std::{NameMsgParams, ProvenanceMsgParams};
167
168 #[test]
169 fn valid_init() {
170 let mut deps = mock_dependencies(&[]);
172
173 let res = instantiate(
175 deps.as_mut(),
176 mock_env(),
177 mock_info("feebucket", &[]),
178 InitMsg {
179 contract_name: "tutorial.sc.pb".into(),
180 purchase_denom: "purchasecoin".into(),
181 merchant_address: "merchant".into(),
182 fee_percent: Decimal::percent(10),
183 },
184 )
185 .unwrap();
186
187 assert_eq!(res.messages.len(), 1);
189 match &res.messages[0].msg {
190 CosmosMsg::Custom(msg) => match &msg.params {
191 ProvenanceMsgParams::Name(p) => match &p {
192 NameMsgParams::BindName { name, .. } => assert_eq!(name, "tutorial.sc.pb"),
193 _ => panic!("unexpected name params"),
194 },
195 _ => panic!("unexpected provenance params"),
196 },
197 _ => panic!("unexpected cosmos message"),
198 }
199 }
200
201 #[test]
202 fn invalid_merchant_init() {
203 let mut deps = mock_dependencies(&[]);
205
206 let err = instantiate(
208 deps.as_mut(),
209 mock_env(),
210 mock_info("merchant", &[]),
211 InitMsg {
212 contract_name: "tutorial.sc.pb".into(),
213 purchase_denom: "purchasecoin".into(),
214 merchant_address: "merchant".into(),
215 fee_percent: Decimal::percent(10),
216 },
217 )
218 .unwrap_err();
219
220 match err {
222 StdError::GenericErr { msg, .. } => {
223 assert_eq!(msg, "merchant address can't be the fee collection address")
224 }
225 _ => panic!("unexpected init error"),
226 }
227 }
228
229 #[test]
230 fn invalid_fee_percent_init() {
231 let mut deps = mock_dependencies(&[]);
233
234 let err = instantiate(
236 deps.as_mut(),
237 mock_env(),
238 mock_info("feebucket", &[]),
239 InitMsg {
240 contract_name: "tutorial.sc.pb".into(),
241 purchase_denom: "purchasecoin".into(),
242 merchant_address: "merchant".into(),
243 fee_percent: Decimal::percent(37), },
245 )
246 .unwrap_err();
247
248 match err {
250 StdError::GenericErr { msg, .. } => {
251 assert_eq!(msg, "fee percent must be > 0.0 and <= 0.25")
252 }
253 _ => panic!("unexpected init error"),
254 }
255 }
256
257 #[test]
258 fn query_test() {
259 let mut deps = mock_dependencies(&[]);
261
262 instantiate(
264 deps.as_mut(),
265 mock_env(),
266 mock_info("feebucket", &[]),
267 InitMsg {
268 contract_name: "tutorial.sc.pb".into(),
269 purchase_denom: "purchasecoin".into(),
270 merchant_address: "merchant".into(),
271 fee_percent: Decimal::percent(10),
272 },
273 )
274 .unwrap(); let bin = query(deps.as_ref(), mock_env(), QueryMsg::QueryRequest {}).unwrap();
278 let resp: QueryResponse = from_binary(&bin).unwrap();
279
280 assert_eq!(resp.merchant_address, Addr::unchecked("merchant"));
282 assert_eq!(resp.purchase_denom, "purchasecoin");
283 assert_eq!(resp.fee_collection_address, Addr::unchecked("feebucket"));
284 assert_eq!(resp.fee_percent, Decimal::percent(10));
285 }
286
287 #[test]
288 fn handle_valid_purchase() {
289 let mut deps = mock_dependencies(&[]);
291
292 instantiate(
294 deps.as_mut(),
295 mock_env(),
296 mock_info("feebucket", &[]),
297 InitMsg {
298 contract_name: "tutorial.sc.pb".into(),
299 purchase_denom: "purchasecoin".into(),
300 merchant_address: "merchant".into(),
301 fee_percent: Decimal::percent(10),
302 },
303 )
304 .unwrap();
305
306 let res = execute(
308 deps.as_mut(),
309 mock_env(),
310 mock_info("consumer", &[coin(100, "purchasecoin")]),
311 ExecuteMsg::Purchase {
312 id: "a7918172-ac09-43f6-bc4b-7ac2fbad17e9".into(),
313 },
314 )
315 .unwrap();
316
317 assert_eq!(res.messages.len(), 2);
319
320 let expected_transfer = coin(90, "purchasecoin");
323 let expected_fees = coin(10, "purchasecoin");
324 res.messages.into_iter().for_each(|msg| match msg.msg {
325 CosmosMsg::Bank(BankMsg::Send {
326 amount, to_address, ..
327 }) => {
328 assert_eq!(amount.len(), 1);
329 if to_address == "merchant" {
330 assert_eq!(amount[0], expected_transfer)
331 } else if to_address == "feebucket" {
332 assert_eq!(amount[0], expected_fees)
333 } else {
334 panic!("unexpected to_address in bank message")
335 }
336 }
337 _ => panic!("unexpected message type"),
338 });
339
340 let expected_purchase_id = "a7918172-ac09-43f6-bc4b-7ac2fbad17e9";
342 res.attributes.into_iter().for_each(|atr| {
343 if atr.key == "purchase_id" {
344 assert_eq!(atr.value, expected_purchase_id)
345 }
346 })
347 }
348
349 #[test]
350 fn handle_invalid_funds() {
351 let mut deps = mock_dependencies(&[]);
353
354 instantiate(
356 deps.as_mut(),
357 mock_env(),
358 mock_info("feebucket", &[]),
359 InitMsg {
360 contract_name: "tutorial.sc.pb".into(),
361 purchase_denom: "purchasecoin".into(),
362 merchant_address: "merchant".into(),
363 fee_percent: Decimal::percent(10),
364 },
365 )
366 .unwrap();
367
368 let err = execute(
370 deps.as_mut(),
371 mock_env(),
372 mock_info("consumer", &[]),
373 ExecuteMsg::Purchase {
374 id: "a7918172-ac09-43f6-bc4b-7ac2fbad17e9".into(),
375 },
376 )
377 .unwrap_err();
378
379 match err {
381 ContractError::Std(StdError::GenericErr { msg, .. }) => {
382 assert_eq!(msg, "no purchase funds sent")
383 }
384 _ => panic!("unexpected handle error"),
385 }
386
387 let err = execute(
389 deps.as_mut(),
390 mock_env(),
391 mock_info("consumer", &[coin(0, "purchasecoin")]),
392 ExecuteMsg::Purchase {
393 id: "a7918172-ac09-43f6-bc4b-7ac2fbad17e9".into(),
394 },
395 )
396 .unwrap_err();
397
398 match err {
400 ContractError::Std(StdError::GenericErr { msg, .. }) => {
401 assert_eq!(msg, "invalid purchase funds: 0purchasecoin")
402 }
403 _ => panic!("unexpected handle error"),
404 }
405
406 let err = execute(
408 deps.as_mut(),
409 mock_env(),
410 mock_info("consumer", &[coin(100, "fakecoin")]),
411 ExecuteMsg::Purchase {
412 id: "a7918172-ac09-43f6-bc4b-7ac2fbad17e9".into(),
413 },
414 )
415 .unwrap_err();
416
417 match err {
419 ContractError::Std(StdError::GenericErr { msg, .. }) => {
420 assert_eq!(msg, "invalid purchase funds: 100fakecoin")
421 }
422 _ => panic!("unexpected handle error"),
423 }
424 }
425}