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::{AmountRange, 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 #[serde(default)]
33 pub paid_to_fees: BorrowAssetAmount,
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
37#[near(serializers = [json, borsh])]
38pub struct YieldWeights {
39 pub supply: NonZeroU16,
40 pub r#static: HashMap<AccountId, u16>,
41}
42
43impl YieldWeights {
44 #[allow(clippy::unwrap_used, reason = "Only used during initial construction")]
47 pub fn new_with_supply_weight(supply: u16) -> Self {
48 Self {
49 supply: supply.try_into().unwrap(),
50 r#static: HashMap::new(),
51 }
52 }
53
54 #[must_use]
55 pub fn with_static(mut self, account_id: AccountId, weight: u16) -> Self {
56 self.r#static.insert(account_id, weight);
57 self
58 }
59
60 pub fn total_weight(&self) -> NonZeroU16 {
61 self.r#static
62 .values()
63 .try_fold(self.supply, |a, b| a.checked_add(*b))
64 .unwrap_or_else(|| crate::panic_with_message("Total weight overflow"))
65 }
66
67 pub fn static_share(&self, account_id: &AccountId) -> Decimal {
68 self.r#static
69 .get(account_id)
70 .map_or(Decimal::ZERO, |weight| {
71 Decimal::from(*weight) / u16::from(self.total_weight())
72 })
73 }
74}
75
76#[derive(Debug)]
79#[near(serializers = [json])]
80pub enum DepositMsg {
81 Supply,
83 Collateralize,
86 Repay,
89 RepayAccount(RepayAccountMsg),
92 Liquidate(LiquidateMsg),
95}
96
97impl DepositMsg {
98 pub fn expects_borrow_asset(&self) -> bool {
99 match self {
100 Self::Supply | Self::Repay | Self::RepayAccount(..) | Self::Liquidate(..) => true,
101 Self::Collateralize => false,
102 }
103 }
104}
105
106#[derive(Debug)]
108#[near(serializers = [json])]
109pub struct RepayAccountMsg {
110 pub account_id: AccountId,
111}
112
113#[derive(Debug)]
115#[near(serializers = [json])]
116pub struct LiquidateMsg {
117 pub account_id: AccountId,
118 pub amount: Option<CollateralAssetAmount>,
121}
122
123#[derive(Clone, Debug)]
124#[near(serializers = [json, borsh])]
125pub struct Withdrawal {
126 pub account_id: AccountId,
127 pub amount_to_account: BorrowAssetAmount,
128 pub amount_to_fees: BorrowAssetAmount,
129}
130
131#[cfg(test)]
132mod tests {
133 use near_sdk::{
134 json_types::U128,
135 serde_json::{self, json, Value},
136 };
137
138 use super::*;
139
140 fn roundtrip(wire: &Value) -> DepositMsg {
145 let parsed: DepositMsg = serde_json::from_value(wire.clone()).unwrap();
146 assert_eq!(&serde_json::to_value(&parsed).unwrap(), wire);
147 parsed
148 }
149
150 fn borrow_asset_metrics_json(paid_to_fees: Option<Value>) -> Value {
151 let mut metrics = json!({
152 "available": "90",
153 "deposited_active": "100",
154 "deposited_incoming": { "7": "10" },
155 "borrowed": "10",
156 });
157 if let Some(paid_to_fees) = paid_to_fees {
158 metrics["paid_to_fees"] = paid_to_fees;
159 }
160 metrics
161 }
162
163 #[test]
164 fn deposit_msg_supply() {
165 let msg = roundtrip(&json!("Supply"));
166 assert!(matches!(msg, DepositMsg::Supply));
167 assert!(msg.expects_borrow_asset());
168 }
169
170 #[test]
171 fn deposit_msg_collateralize() {
172 let msg = roundtrip(&json!("Collateralize"));
173 assert!(matches!(msg, DepositMsg::Collateralize));
174 assert!(!msg.expects_borrow_asset());
175 }
176
177 #[test]
178 fn deposit_msg_repay() {
179 let msg = roundtrip(&json!("Repay"));
180 assert!(matches!(msg, DepositMsg::Repay));
181 assert!(msg.expects_borrow_asset());
182 }
183
184 #[test]
185 fn deposit_msg_repay_account() {
186 let msg = roundtrip(&json!({ "RepayAccount": { "account_id": "borrow_user.near" } }));
187 let DepositMsg::RepayAccount(RepayAccountMsg { account_id }) = &msg else {
188 panic!("expected RepayAccount, got {msg:?}");
189 };
190 assert_eq!(account_id.as_str(), "borrow_user.near");
191 assert!(msg.expects_borrow_asset());
192 }
193
194 #[test]
195 fn deposit_msg_liquidate() {
196 let msg = roundtrip(&json!({
197 "Liquidate": { "account_id": "borrow_user.near", "amount": U128(1_000_000) },
198 }));
199 let DepositMsg::Liquidate(LiquidateMsg { account_id, amount }) = &msg else {
200 panic!("expected Liquidate, got {msg:?}");
201 };
202 assert_eq!(account_id.as_str(), "borrow_user.near");
203 assert_eq!(*amount, Some(CollateralAssetAmount::new(1_000_000)));
204 assert!(msg.expects_borrow_asset());
205 }
206
207 #[test]
208 fn deposit_msg_liquidate_whole_position() {
209 let msg: DepositMsg =
212 serde_json::from_value(json!({ "Liquidate": { "account_id": "borrow_user.near" } }))
213 .unwrap();
214 let DepositMsg::Liquidate(LiquidateMsg { amount, .. }) = &msg else {
215 panic!("expected Liquidate, got {msg:?}");
216 };
217 assert_eq!(*amount, None);
218 }
219
220 #[test]
221 fn borrow_asset_metrics_legacy_json() {
222 let metrics: BorrowAssetMetrics =
223 serde_json::from_value(borrow_asset_metrics_json(None)).unwrap();
224
225 assert_eq!(u128::from(metrics.available), 90);
226 assert_eq!(u128::from(metrics.deposited_active), 100);
227 assert_eq!(u128::from(metrics.deposited_incoming[&7]), 10);
228 assert_eq!(u128::from(metrics.borrowed), 10);
229 assert!(metrics.paid_to_fees.is_zero());
230 }
231
232 #[test]
233 fn borrow_asset_metrics_paid_to_fees_json() {
234 for expected_paid_to_fees in [0, u128::MAX] {
235 let paid_to_fees = expected_paid_to_fees.to_string();
236 let metrics: BorrowAssetMetrics =
237 serde_json::from_value(borrow_asset_metrics_json(Some(json!(paid_to_fees))))
238 .unwrap();
239
240 assert_eq!(u128::from(metrics.paid_to_fees), expected_paid_to_fees);
241 assert_eq!(
242 serde_json::to_value(&metrics).unwrap()["paid_to_fees"],
243 paid_to_fees,
244 );
245 }
246 }
247
248 #[test]
249 fn borrow_asset_metrics_rejects_malformed_paid_to_fees() {
250 for paid_to_fees in [Value::Null, json!("not-an-amount")] {
251 assert!(
252 serde_json::from_value::<BorrowAssetMetrics>(borrow_asset_metrics_json(Some(
253 paid_to_fees
254 ),))
255 .is_err()
256 );
257 }
258 }
259}