sei_integration_tests/
module.rs

1use anyhow::Result as AnyResult;
2use base64::{engine::general_purpose, Engine as _};
3use cosmwasm_std::{
4    from_json, to_json_binary, Addr, Api, BankMsg, Binary, BlockInfo, Coin, CosmosMsg, CustomQuery,
5    Decimal, Querier, Storage, Uint128, Uint64,
6};
7use cw_multi_test::{AppResponse, BankSudo, CosmosRouter, Module, SudoMsg};
8use schemars::JsonSchema;
9use sei_cosmwasm::{
10    Cancellation, DenomOracleExchangeRatePair, DexPair, DexTwap, DexTwapsResponse, Epoch,
11    EpochResponse, EvmAddressResponse, ExchangeRatesResponse, GetOrderByIdResponse,
12    GetOrdersResponse, OracleTwap, OracleTwapsResponse, Order, OrderResponse,
13    OrderSimulationResponse, OrderStatus, PositionDirection, SeiAddressResponse, SeiMsg, SeiQuery,
14    SeiQueryWrapper, StaticCallResponse, SudoMsg as SeiSudoMsg,
15};
16use serde::de::DeserializeOwned;
17use std::{
18    collections::HashMap,
19    fmt::Debug,
20    ops::{Add, Div, Mul, Sub},
21};
22
23pub struct SeiModule {
24    epoch: Epoch,
25    exchange_rates: HashMap<String, Vec<DenomOracleExchangeRatePair>>,
26}
27
28const GENESIS_EPOCH: Epoch = Epoch {
29    genesis_time: String::new(),
30    duration: 60,
31    current_epoch: 1,
32    current_epoch_start_time: String::new(),
33    current_epoch_height: 1,
34};
35
36pub const EVM_ADDRESS: &str = "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B";
37pub const SEI_ADDRESS: &str = "sei1vzxkv3lxccnttr9rs0002s93sgw72h7ghukuhs";
38
39impl SeiModule {
40    pub fn new() -> Self {
41        SeiModule {
42            epoch: GENESIS_EPOCH,
43            exchange_rates: HashMap::new(),
44        }
45    }
46
47    pub fn new_with_oracle_exchange_rates(rates: Vec<DenomOracleExchangeRatePair>) -> Self {
48        let mut exchange_rates: HashMap<String, Vec<DenomOracleExchangeRatePair>> = HashMap::new();
49
50        for rate in rates {
51            let arr = exchange_rates
52                .entry(rate.denom.clone())
53                .or_insert_with(Vec::new);
54
55            match arr.binary_search_by(|x| {
56                rate.oracle_exchange_rate
57                    .last_update
58                    .cmp(&x.oracle_exchange_rate.last_update)
59            }) {
60                Ok(_) => {}
61                Err(pos) => arr.insert(pos, rate.clone()),
62            };
63        }
64
65        SeiModule {
66            epoch: GENESIS_EPOCH,
67            exchange_rates: exchange_rates,
68        }
69    }
70
71    pub fn set_epoch(&self, new_epoch: Epoch) -> Self {
72        SeiModule {
73            epoch: new_epoch,
74            exchange_rates: (&self.exchange_rates).clone(),
75        }
76    }
77}
78
79impl Default for SeiModule {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85impl Module for SeiModule {
86    type ExecT = SeiMsg;
87    type QueryT = SeiQueryWrapper;
88    type SudoT = SeiSudoMsg;
89
90    fn execute<ExecC, QueryC>(
91        &self,
92        api: &dyn Api,
93        storage: &mut dyn Storage,
94        router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
95        block: &BlockInfo,
96        sender: Addr,
97        msg: Self::ExecT,
98    ) -> AnyResult<AppResponse>
99    where
100        ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static,
101        QueryC: CustomQuery + DeserializeOwned + 'static,
102    {
103        match msg {
104            SeiMsg::PlaceOrders {
105                orders,
106                funds,
107                contract_address,
108            } => {
109                return execute_place_orders_helper(
110                    storage,
111                    block,
112                    orders,
113                    funds,
114                    contract_address,
115                );
116            }
117            SeiMsg::CancelOrders {
118                cancellations,
119                contract_address,
120            } => {
121                return execute_cancel_orders_helper(storage, cancellations, contract_address);
122            }
123            SeiMsg::CreateDenom { subdenom } => {
124                return execute_create_denom_helper(storage, sender, subdenom);
125            }
126            SeiMsg::MintTokens { amount } => {
127                return execute_mint_tokens_helper(api, storage, router, block, sender, amount);
128            }
129            SeiMsg::BurnTokens { amount } => {
130                return execute_burn_tokens_helper(api, storage, router, block, sender, amount);
131            }
132            _ => panic!("Unexpected custom exec msg"),
133        }
134    }
135
136    fn query(
137        &self,
138        _api: &dyn Api,
139        storage: &dyn Storage,
140        _querier: &dyn Querier,
141        block: &BlockInfo,
142        request: Self::QueryT,
143    ) -> AnyResult<Binary> {
144        match request.query_data {
145            SeiQuery::ExchangeRates {} => Ok(to_json_binary(&get_exchange_rates(
146                self.exchange_rates.clone(),
147            ))?),
148            SeiQuery::OracleTwaps { lookback_seconds } => Ok(to_json_binary(&get_oracle_twaps(
149                block,
150                self.exchange_rates.clone(),
151                lookback_seconds,
152            ))?),
153            SeiQuery::DexTwaps {
154                contract_address,
155                lookback_seconds,
156            } => Ok(to_json_binary(&get_dex_twaps(
157                storage,
158                block,
159                contract_address,
160                lookback_seconds,
161            ))?),
162            SeiQuery::OrderSimulation {
163                order,
164                contract_address,
165            } => Ok(to_json_binary(&get_order_simulation(
166                storage,
167                order,
168                contract_address,
169            ))?),
170            SeiQuery::Epoch {} => return query_get_epoch_helper(self.epoch.clone()),
171            SeiQuery::GetOrders {
172                contract_address,
173                account,
174            } => {
175                return query_get_orders_helper(storage, contract_address, account);
176            }
177            // TODO: Implement Set and Get latest price in integration tests
178            SeiQuery::GetLatestPrice { .. } => {
179                panic!("Get Latest Price Query not implemented")
180            }
181            SeiQuery::GetOrderById {
182                contract_address,
183                price_denom,
184                asset_denom,
185                id,
186            } => {
187                return query_get_order_by_id_helper(
188                    storage,
189                    contract_address,
190                    price_denom,
191                    asset_denom,
192                    id,
193                );
194            }
195            SeiQuery::StaticCall { .. } => Ok(to_json_binary(&get_static_call_response())?),
196            SeiQuery::GetEvmAddress { sei_address } => {
197                Ok(to_json_binary(&get_evm_address(sei_address))?)
198            }
199            SeiQuery::GetSeiAddress { evm_address } => {
200                Ok(to_json_binary(&get_sei_address(evm_address))?)
201            }
202            // TODO: Implement get denom authority metadata in integration tests
203            SeiQuery::DenomAuthorityMetadata { .. } => {
204                panic!("Denom Authority Metadata not implemented")
205            }
206            // TODO: Implement get denom from creator in integration tests
207            SeiQuery::DenomsFromCreator { .. } => {
208                panic!("Denoms From Creator not implemented")
209            }
210            _ => panic!("Unexpected custom query msg"),
211        }
212    }
213
214    fn sudo<ExecC, QueryC>(
215        &self,
216        _api: &dyn Api,
217        _storage: &mut dyn Storage,
218        _router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
219        _block: &BlockInfo,
220        msg: Self::SudoT,
221    ) -> AnyResult<AppResponse>
222    where
223        ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static,
224        QueryC: CustomQuery + DeserializeOwned + 'static,
225    {
226        match msg {
227            SeiSudoMsg::Settlement {
228                epoch: _,
229                entries: _,
230            } => Ok(AppResponse {
231                events: vec![],
232                data: None,
233            }),
234            SeiSudoMsg::BulkOrderPlacements {
235                orders: _,
236                deposits: _,
237            } => Ok(AppResponse {
238                events: vec![],
239                data: None,
240            }),
241            SeiSudoMsg::BulkOrderCancellations { ids: _ } => Ok(AppResponse {
242                events: vec![],
243                data: None,
244            }),
245        }
246    }
247}
248
249// Helper functions
250
251// Dex Module Msg
252
253// Execute: PlaceOrders()
254fn execute_place_orders_helper(
255    storage: &mut dyn Storage,
256    block: &BlockInfo,
257    orders: Vec<Order>,
258    _funds: Vec<Coin>,
259    contract_address: Addr,
260) -> AnyResult<AppResponse> {
261    // Storage:
262    // OrderIdCounter -> OrderId
263    // contract_address + "-" + OrderResponses -> OrderResponse[]
264    // contract_address + "-" + OrderResponseById + "-" + Price Denom + "-" + Asset Denom + "-" + OrderId -> OrderResponse
265    // "OrderTimestamp-" + OrderId -> OrderTimestamp
266
267    // Get latest order id
268    let mut latest_order_id: u64 = 0;
269    let curr = storage.get("OrderIdCounter".as_bytes());
270    if storage.get("OrderIdCounter".as_bytes()).is_some() {
271        latest_order_id = String::from_utf8(curr.unwrap_or_default())
272            .unwrap_or_default()
273            .parse::<u64>()
274            .unwrap();
275    }
276
277    // get existing orders
278    let order_responses_key = contract_address.to_string() + "-" + "OrderResponses";
279
280    let mut order_responses: Vec<OrderResponse> = Vec::new();
281    let existing_order_responses = storage.get(order_responses_key.as_bytes());
282    if existing_order_responses.is_some() {
283        //let order_responses_key = contract_address.to_string() + "-" + "OrderResponses";
284        let responses_json: String =
285            serde_json::from_slice(&existing_order_responses.clone().unwrap()).unwrap();
286        order_responses = serde_json::from_str(&responses_json).unwrap();
287    }
288    // Iterate through orders, make OrderResponse
289    for order in orders.iter() {
290        let order_response = OrderResponse {
291            id: latest_order_id,
292            status: OrderStatus::Placed,
293            price: order.price,
294            quantity: order.quantity,
295            price_denom: order.price_denom.clone(),
296            asset_denom: order.asset_denom.clone(),
297            order_type: order.order_type,
298            position_direction: order.position_direction,
299            data: order.data.clone(),
300            account: "test account".to_string(),
301            contract_address: "test contract".to_string(),
302            status_description: "desc".to_string(),
303        };
304        order_responses.push(order_response.clone());
305
306        // update GetOrderById() -> OrderResponse storage
307        let response_json = serde_json::to_string(&order_response);
308        let order_id_key = contract_address.to_string()
309            + "-"
310            + "OrderResponseById"
311            + "-"
312            + &order.price_denom.clone()
313            + "-"
314            + &order.asset_denom.clone()
315            + "-"
316            + &latest_order_id.to_string();
317        storage.set(
318            order_id_key.as_bytes(),
319            &serde_json::to_vec(&response_json.unwrap_or_default()).unwrap(),
320        );
321        storage.set(
322            format!("OrderTimestamp-{}", latest_order_id).as_bytes(),
323            &block.time.seconds().to_be_bytes(),
324        );
325
326        latest_order_id += 1;
327    }
328
329    let responses_json = serde_json::to_string(&order_responses);
330
331    // update GetOrders() -> OrderResponse[] storage
332    storage.set(
333        order_responses_key.as_bytes(),
334        &serde_json::to_vec(&responses_json.unwrap_or_default()).unwrap(),
335    );
336    // update OrderIdCounter -> latest_order_id storage
337    storage.set(
338        "OrderIdCounter".as_bytes(),
339        latest_order_id.to_string().as_bytes(),
340    );
341
342    Ok(AppResponse {
343        events: vec![],
344        data: Some(to_json_binary(&contract_address).unwrap()),
345    })
346}
347
348// Execute: CancelOrders()
349fn execute_cancel_orders_helper(
350    storage: &mut dyn Storage,
351    cancellations: Vec<Cancellation>,
352    contract_address: Addr,
353) -> AnyResult<AppResponse> {
354    // get existing orders
355    let order_responses_key = contract_address.to_string() + "-" + "OrderResponses";
356
357    let existing_order_responses = storage.get(order_responses_key.as_bytes());
358    if !existing_order_responses.is_some() {
359        return Err(anyhow::anyhow!(
360            "CancelOrders: orders for contract_address do not exist"
361        ));
362    }
363
364    let responses_json: String =
365        serde_json::from_slice(&existing_order_responses.clone().unwrap()).unwrap();
366    let mut order_responses: Vec<OrderResponse> = serde_json::from_str(&responses_json).unwrap();
367
368    let order_ids: Vec<u64> = cancellations.iter().map(|c| -> u64 { c.id }).collect();
369    for order_id in order_ids.clone() {
370        let order_response: Vec<OrderResponse> = order_responses
371            .clone()
372            .into_iter()
373            .filter(|o| order_id.clone() == o.id)
374            .collect();
375        let order_id_key = contract_address.to_string()
376            + "-"
377            + "OrderResponseById"
378            + "-"
379            + &order_response[0].price_denom.clone()
380            + "-"
381            + &order_response[0].asset_denom.clone()
382            + "-"
383            + &order_id.to_string();
384        // Remove individual for GetOrderById()
385        storage.remove(order_id_key.as_bytes());
386    }
387
388    order_responses = order_responses
389        .into_iter()
390        .filter(|o| !order_ids.contains(&o.id))
391        .collect();
392
393    let responses_json = serde_json::to_string(&order_responses);
394
395    // update GetOrders() -> OrderResponse[] storage
396    storage.set(
397        order_responses_key.as_bytes(),
398        &serde_json::to_vec(&responses_json.unwrap_or_default()).unwrap(),
399    );
400
401    Ok(AppResponse {
402        events: vec![],
403        data: Some(to_json_binary(&contract_address).unwrap()),
404    })
405}
406
407// Oracle Module
408
409fn get_exchange_rates(
410    rates: HashMap<String, Vec<DenomOracleExchangeRatePair>>,
411) -> ExchangeRatesResponse {
412    let mut exchange_rates: Vec<DenomOracleExchangeRatePair> = Vec::new();
413
414    for key in rates.keys() {
415        let rate = rates.get(key).unwrap();
416        exchange_rates.push(rate[0].clone());
417    }
418
419    ExchangeRatesResponse {
420        denom_oracle_exchange_rate_pairs: exchange_rates,
421    }
422}
423
424fn get_oracle_twaps(
425    block: &BlockInfo,
426    rates: HashMap<String, Vec<DenomOracleExchangeRatePair>>,
427    lookback_seconds: u64,
428) -> OracleTwapsResponse {
429    let mut oracle_twaps: Vec<OracleTwap> = Vec::new();
430    let lbs = lookback_seconds as u64;
431
432    for key in rates.keys() {
433        let pair_rates = rates.get(key).unwrap();
434        let mut sum = Decimal::zero();
435        let start: u64 = block.time.seconds();
436        let mut time: u64 = block.time.seconds();
437        let mut last_rate = Decimal::zero();
438
439        if pair_rates[0].oracle_exchange_rate.last_update < Uint64::new(start - lbs) {
440            oracle_twaps.push(OracleTwap {
441                denom: key.clone(),
442                twap: pair_rates[0].oracle_exchange_rate.exchange_rate,
443                lookback_seconds: lookback_seconds,
444            });
445            continue;
446        }
447
448        // Average prices of rates for the past lookback_seconds
449        for rate in pair_rates {
450            last_rate = rate.oracle_exchange_rate.exchange_rate;
451            if Uint64::new(start) - rate.oracle_exchange_rate.last_update < Uint64::new(lbs) {
452                sum += last_rate.mul(Decimal::from_ratio(
453                    Uint128::new((time - rate.oracle_exchange_rate.last_update.u64()).into()),
454                    Uint128::one(),
455                ));
456                time = rate.oracle_exchange_rate.last_update.u64();
457            } else {
458                break;
459            }
460        }
461
462        if Uint64::new(start - time) < Uint64::new(lbs) {
463            let sec: u64 = lbs;
464            let diff = sec.sub(start - time);
465            sum += last_rate.mul(Decimal::from_ratio(
466                Uint128::new(diff.into()),
467                Uint128::one(),
468            ));
469        }
470
471        oracle_twaps.push(OracleTwap {
472            denom: key.clone(),
473            twap: sum.div(Decimal::from_ratio(
474                Uint128::new(lbs.into()),
475                Uint128::one(),
476            )),
477            lookback_seconds: lookback_seconds,
478        });
479    }
480
481    OracleTwapsResponse {
482        oracle_twaps: oracle_twaps,
483    }
484}
485
486fn get_dex_twaps(
487    storage: &dyn Storage,
488    block: &BlockInfo,
489    contract_address: Addr,
490    lookback_seconds: u64,
491) -> DexTwapsResponse {
492    let mut dex_twaps: HashMap<(String, String), Decimal> = HashMap::new();
493    let mut prev_time = block.time.seconds();
494
495    let order_response: GetOrdersResponse = from_json(
496        &query_get_orders_helper(storage, contract_address, Addr::unchecked("")).unwrap(),
497    )
498    .unwrap();
499
500    let mut orders = order_response.orders.clone();
501    orders.sort_by(|a, b| b.id.cmp(&a.id));
502
503    for order in orders {
504        let timestamp = u64::from_be_bytes(
505            storage
506                .get(format!("OrderTimestamp-{}", order.id).as_bytes())
507                .unwrap()
508                .try_into()
509                .unwrap(),
510        );
511
512        let mut update_fn = |time: u64| {
513            if !dex_twaps.contains_key(&(order.asset_denom.clone(), order.price_denom.clone())) {
514                dex_twaps.insert(
515                    (order.asset_denom.clone(), order.price_denom.clone()),
516                    Decimal::zero(),
517                );
518            }
519
520            let sum = dex_twaps
521                .get(&(order.asset_denom.clone(), order.price_denom.clone()))
522                .unwrap();
523
524            let new_sum = sum.add(order.price.mul(Decimal::from_ratio(time, 1u64)));
525
526            dex_twaps.insert(
527                (order.asset_denom.clone(), order.price_denom.clone()),
528                new_sum,
529            );
530        };
531
532        if block.time.seconds() - timestamp >= lookback_seconds {
533            update_fn(lookback_seconds - (block.time.seconds() - prev_time));
534            prev_time = timestamp;
535        } else if block.time.seconds() - prev_time < lookback_seconds {
536            update_fn(prev_time - timestamp);
537            prev_time = timestamp;
538        }
539    }
540
541    let mut twaps: Vec<DexTwap> = Vec::new();
542    for key in dex_twaps.keys() {
543        let sum = dex_twaps.get(key).unwrap();
544        twaps.push(DexTwap {
545            pair: DexPair {
546                asset_denom: key.0.clone(),
547                price_denom: key.1.clone(),
548                price_tick_size: Decimal::from_ratio(1u128, 10000u128),
549                quantity_tick_size: Decimal::from_ratio(1u128, 10000u128),
550            },
551            twap: sum.div(Decimal::from_ratio(lookback_seconds, 1u64)),
552            lookback_seconds: lookback_seconds,
553        });
554    }
555
556    DexTwapsResponse { twaps }
557}
558
559fn get_order_simulation(
560    storage: &dyn Storage,
561    order: Order,
562    contract_address: Addr,
563) -> OrderSimulationResponse {
564    let mut executed_quantity = Decimal::zero();
565
566    let orders: GetOrdersResponse = from_json(
567        &query_get_orders_helper(storage, contract_address, Addr::unchecked("")).unwrap(),
568    )
569    .unwrap();
570
571    let valid_orders = if order.position_direction == PositionDirection::Long {
572        PositionDirection::Short
573    } else {
574        PositionDirection::Long
575    };
576
577    for order_response in orders.orders {
578        if order_response.position_direction == valid_orders {
579            if (order_response.position_direction == PositionDirection::Long
580                && order.price <= order_response.price)
581                || (order_response.position_direction == PositionDirection::Short
582                    && order.price >= order_response.price)
583            {
584                executed_quantity += order_response.quantity;
585            }
586        }
587    }
588
589    OrderSimulationResponse {
590        executed_quantity: if executed_quantity > order.quantity {
591            order.quantity
592        } else {
593            executed_quantity
594        },
595    }
596}
597
598// Query: GetOrders()
599fn query_get_orders_helper(
600    storage: &dyn Storage,
601    contract_address: Addr,
602    _account: Addr,
603) -> AnyResult<Binary> {
604    let order_responses_key = contract_address.to_string() + "-" + "OrderResponses";
605    let existing_order_responses = storage.get(order_responses_key.as_bytes());
606    if !existing_order_responses.is_some() {
607        return Err(anyhow::anyhow!(
608            "GetOrders: orders for contract_address do not exist"
609        ));
610    }
611    let responses_json: String =
612        serde_json::from_slice(&existing_order_responses.clone().unwrap()).unwrap();
613
614    let order_responses: Vec<OrderResponse> = serde_json::from_str(&responses_json).unwrap();
615
616    return Ok(to_json_binary(&GetOrdersResponse {
617        orders: order_responses,
618    })?);
619}
620
621// Query: GetOrderById()
622fn query_get_order_by_id_helper(
623    storage: &dyn Storage,
624    contract_address: Addr,
625    price_denom: String,
626    asset_denom: String,
627    id: u64,
628) -> AnyResult<Binary> {
629    let order_id_key = contract_address.to_string()
630        + "-"
631        + "OrderResponseById"
632        + "-"
633        + &price_denom
634        + "-"
635        + &asset_denom
636        + "-"
637        + &id.to_string();
638    let existing_order_response = storage.get(order_id_key.as_bytes());
639
640    if !existing_order_response.is_some() {
641        return Err(anyhow::anyhow!("GetOrderById: order for id does not exist"));
642    }
643
644    let response_json: String =
645        serde_json::from_slice(&existing_order_response.clone().unwrap()).unwrap();
646
647    let order_response: OrderResponse = serde_json::from_str(&response_json).unwrap();
648
649    return Ok(to_json_binary(&GetOrderByIdResponse {
650        order: order_response,
651    })?);
652}
653
654// Epoch Module Queries
655
656fn get_epoch(epoch: Epoch) -> EpochResponse {
657    EpochResponse { epoch: epoch }
658}
659
660fn get_static_call_response() -> StaticCallResponse {
661    StaticCallResponse {
662        encoded_data: general_purpose::STANDARD.encode(b"static call response"),
663    }
664}
665
666fn get_evm_address(sei_address: String) -> EvmAddressResponse {
667    let (evm_address, associated) = match sei_address.as_str() {
668        SEI_ADDRESS => (EVM_ADDRESS.to_string(), true),
669        _ => (String::new(), false), // default case
670    };
671
672    EvmAddressResponse {
673        evm_address,
674        associated,
675    }
676}
677
678fn get_sei_address(evm_address: String) -> SeiAddressResponse {
679    let (sei_address, associated) = match evm_address.as_str() {
680        EVM_ADDRESS => (SEI_ADDRESS.to_string(), true),
681        _ => (String::new(), false), // default case
682    };
683
684    SeiAddressResponse {
685        sei_address,
686        associated,
687    }
688}
689
690// Query: GetEpoch()
691fn query_get_epoch_helper(epoch: Epoch) -> AnyResult<Binary> {
692    return Ok(to_json_binary(&get_epoch(epoch))?);
693}
694
695// TokenFactory Msg
696
697// Execute: CreateDenom()
698fn execute_create_denom_helper(
699    storage: &mut dyn Storage,
700    sender: Addr,
701    subdenom: String,
702) -> AnyResult<AppResponse> {
703    let denom = format!("factory/{}/{}", sender, subdenom);
704    if storage.get(denom.as_bytes()).is_some() {
705        return Err(anyhow::anyhow!("denom already exists"));
706    }
707    storage.set(denom.as_bytes(), sender.to_string().as_bytes());
708    Ok(AppResponse {
709        events: vec![],
710        data: Some(to_json_binary(&denom).unwrap()),
711    })
712}
713
714// Execute: MintTokens()
715fn execute_mint_tokens_helper<ExecC, QueryC>(
716    api: &dyn Api,
717    storage: &mut dyn Storage,
718    router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
719    block: &BlockInfo,
720    sender: Addr,
721    amount: Coin,
722) -> AnyResult<AppResponse>
723where
724    ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static,
725    QueryC: CustomQuery + DeserializeOwned + 'static,
726{
727    let owner = storage.get(amount.denom.as_bytes());
728    if owner.is_none() || owner.unwrap() != sender.to_string().as_bytes() {
729        return Err(anyhow::anyhow!(
730            "Must be owner of coin factory denom to mint"
731        ));
732    }
733    router.sudo(
734        api,
735        storage,
736        block,
737        SudoMsg::Bank(BankSudo::Mint {
738            to_address: sender.to_string(),
739            amount: vec![amount],
740        }),
741    )
742}
743
744// Execute: BurnTokens()
745fn execute_burn_tokens_helper<ExecC, QueryC>(
746    api: &dyn Api,
747    storage: &mut dyn Storage,
748    router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
749    block: &BlockInfo,
750    sender: Addr,
751    amount: Coin,
752) -> AnyResult<AppResponse>
753where
754    ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static,
755    QueryC: CustomQuery + DeserializeOwned + 'static,
756{
757    let owner = storage.get(amount.denom.as_bytes());
758    if owner.is_none() || owner.unwrap() != sender.to_string().as_bytes() {
759        return Err(anyhow::anyhow!(
760            "Must be owner of coin factory denom to burn"
761        ));
762    }
763    Ok(router
764        .execute(
765            api,
766            storage,
767            block,
768            sender,
769            CosmosMsg::Bank(BankMsg::Burn {
770                amount: vec![amount],
771            }),
772        )
773        .unwrap())
774}