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 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 SeiQuery::DenomAuthorityMetadata { .. } => {
204 panic!("Denom Authority Metadata not implemented")
205 }
206 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
249fn 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 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 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 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 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 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 storage.set(
333 order_responses_key.as_bytes(),
334 &serde_json::to_vec(&responses_json.unwrap_or_default()).unwrap(),
335 );
336 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
348fn execute_cancel_orders_helper(
350 storage: &mut dyn Storage,
351 cancellations: Vec<Cancellation>,
352 contract_address: Addr,
353) -> AnyResult<AppResponse> {
354 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 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 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
407fn 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 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
598fn 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
621fn 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
654fn 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), };
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), };
683
684 SeiAddressResponse {
685 sei_address,
686 associated,
687 }
688}
689
690fn query_get_epoch_helper(epoch: Epoch) -> AnyResult<Binary> {
692 return Ok(to_json_binary(&get_epoch(epoch))?);
693}
694
695fn 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
714fn 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
744fn 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}