templar_common/market/
mod.rs1use std::collections::HashMap;
2use std::num::NonZeroU16;
3
4use near_sdk::{near, AccountId};
5use templar_primitives::number::Decimal;
6
7use crate::asset::{BorrowAssetAmount, CollateralAssetAmount};
8mod configuration;
9pub use configuration::{MarketConfiguration, ValidAmountRange, APY_LIMIT};
10mod external;
11pub use external::*;
12mod r#impl;
13pub use r#impl::*;
14mod price_oracle_configuration;
15pub use price_oracle_configuration::PriceOracleConfiguration;
16
17pub mod error {
18 pub use super::configuration::error::*;
19 pub use super::price_oracle_configuration::error::*;
20}
21
22#[derive(Clone, Debug)]
23#[near(serializers = [borsh, json])]
24pub struct BorrowAssetMetrics {
25 pub available: BorrowAssetAmount,
26 pub deposited_active: BorrowAssetAmount,
27 pub deposited_incoming: HashMap<u32, BorrowAssetAmount>,
28 pub borrowed: BorrowAssetAmount,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32#[near(serializers = [json, borsh])]
33pub struct YieldWeights {
34 pub supply: NonZeroU16,
35 pub r#static: HashMap<AccountId, u16>,
36}
37
38impl YieldWeights {
39 #[allow(clippy::unwrap_used, reason = "Only used during initial construction")]
42 pub fn new_with_supply_weight(supply: u16) -> Self {
43 Self {
44 supply: supply.try_into().unwrap(),
45 r#static: HashMap::new(),
46 }
47 }
48
49 #[must_use]
50 pub fn with_static(mut self, account_id: AccountId, weight: u16) -> Self {
51 self.r#static.insert(account_id, weight);
52 self
53 }
54
55 pub fn total_weight(&self) -> NonZeroU16 {
56 self.r#static
57 .values()
58 .try_fold(self.supply, |a, b| a.checked_add(*b))
59 .unwrap_or_else(|| crate::panic_with_message("Total weight overflow"))
60 }
61
62 pub fn static_share(&self, account_id: &AccountId) -> Decimal {
63 self.r#static
64 .get(account_id)
65 .map_or(Decimal::ZERO, |weight| {
66 Decimal::from(*weight) / u16::from(self.total_weight())
67 })
68 }
69}
70
71#[derive(Debug)]
74#[near(serializers = [json])]
75pub enum DepositMsg {
76 Supply,
78 Collateralize,
81 Repay,
84 RepayAccount(RepayAccountMsg),
87 Liquidate(LiquidateMsg),
90}
91
92impl DepositMsg {
93 pub fn expects_borrow_asset(&self) -> bool {
94 match self {
95 Self::Supply | Self::Repay | Self::RepayAccount(..) | Self::Liquidate(..) => true,
96 Self::Collateralize => false,
97 }
98 }
99}
100
101#[derive(Debug)]
103#[near(serializers = [json])]
104pub struct RepayAccountMsg {
105 pub account_id: AccountId,
106}
107
108#[derive(Debug)]
110#[near(serializers = [json])]
111pub struct LiquidateMsg {
112 pub account_id: AccountId,
113 pub amount: Option<CollateralAssetAmount>,
116}
117
118#[derive(Clone, Debug)]
119#[near(serializers = [json, borsh])]
120pub struct Withdrawal {
121 pub account_id: AccountId,
122 pub amount_to_account: BorrowAssetAmount,
123 pub amount_to_fees: BorrowAssetAmount,
124}
125
126#[cfg(test)]
127mod tests {
128 use near_sdk::{
129 json_types::U128,
130 serde_json::{self, json, Value},
131 };
132
133 use super::*;
134
135 fn roundtrip(wire: &Value) -> DepositMsg {
140 let parsed: DepositMsg = serde_json::from_value(wire.clone()).unwrap();
141 assert_eq!(&serde_json::to_value(&parsed).unwrap(), wire);
142 parsed
143 }
144
145 #[test]
146 fn deposit_msg_supply() {
147 let msg = roundtrip(&json!("Supply"));
148 assert!(matches!(msg, DepositMsg::Supply));
149 assert!(msg.expects_borrow_asset());
150 }
151
152 #[test]
153 fn deposit_msg_collateralize() {
154 let msg = roundtrip(&json!("Collateralize"));
155 assert!(matches!(msg, DepositMsg::Collateralize));
156 assert!(!msg.expects_borrow_asset());
157 }
158
159 #[test]
160 fn deposit_msg_repay() {
161 let msg = roundtrip(&json!("Repay"));
162 assert!(matches!(msg, DepositMsg::Repay));
163 assert!(msg.expects_borrow_asset());
164 }
165
166 #[test]
167 fn deposit_msg_repay_account() {
168 let msg = roundtrip(&json!({ "RepayAccount": { "account_id": "borrow_user.near" } }));
169 let DepositMsg::RepayAccount(RepayAccountMsg { account_id }) = &msg else {
170 panic!("expected RepayAccount, got {msg:?}");
171 };
172 assert_eq!(account_id.as_str(), "borrow_user.near");
173 assert!(msg.expects_borrow_asset());
174 }
175
176 #[test]
177 fn deposit_msg_liquidate() {
178 let msg = roundtrip(&json!({
179 "Liquidate": { "account_id": "borrow_user.near", "amount": U128(1_000_000) },
180 }));
181 let DepositMsg::Liquidate(LiquidateMsg { account_id, amount }) = &msg else {
182 panic!("expected Liquidate, got {msg:?}");
183 };
184 assert_eq!(account_id.as_str(), "borrow_user.near");
185 assert_eq!(*amount, Some(CollateralAssetAmount::new(1_000_000)));
186 assert!(msg.expects_borrow_asset());
187 }
188
189 #[test]
190 fn deposit_msg_liquidate_whole_position() {
191 let msg: DepositMsg =
194 serde_json::from_value(json!({ "Liquidate": { "account_id": "borrow_user.near" } }))
195 .unwrap();
196 let DepositMsg::Liquidate(LiquidateMsg { amount, .. }) = &msg else {
197 panic!("expected Liquidate, got {msg:?}");
198 };
199 assert_eq!(*amount, None);
200 }
201}