diff --git a/Cargo.lock b/Cargo.lock index 7071a3f4..ffaddc05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1618,7 +1618,7 @@ dependencies = [ "injective-math 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "injective-std 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)", "injective-test-tube", - "injective-testing 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "injective-testing 0.2.1", "prost 0.12.6", "schemars", "serde 1.0.204", @@ -1732,6 +1732,8 @@ dependencies = [ [[package]] name = "injective-testing" version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7fac0e025e0755fad2a3e9f6fbbb052fc0cbff9517b74c7d9cd2076d0aee2e" dependencies = [ "anyhow", "base64 0.13.1", @@ -1747,9 +1749,7 @@ dependencies = [ [[package]] name = "injective-testing" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7fac0e025e0755fad2a3e9f6fbbb052fc0cbff9517b74c7d9cd2076d0aee2e" +version = "1.0.0" dependencies = [ "anyhow", "base64 0.13.1", @@ -1757,6 +1757,9 @@ dependencies = [ "cw-multi-test", "injective-cosmwasm 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "injective-math 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "injective-std 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)", + "injective-test-tube", + "prost 0.12.6", "rand 0.4.6", "secp256k1", "serde 1.0.204", diff --git a/packages/injective-testing/CHANGELOG.md b/packages/injective-testing/CHANGELOG.md new file mode 100644 index 00000000..5715199e --- /dev/null +++ b/packages/injective-testing/CHANGELOG.md @@ -0,0 +1,12 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [1.0.0] - 2024-08-23 + +### Changed + +- Restructure of architecture to improve organisation of helper functions and also to enable re-use with multiple projects diff --git a/packages/injective-testing/Cargo.toml b/packages/injective-testing/Cargo.toml index 3b74e360..c5c284d4 100644 --- a/packages/injective-testing/Cargo.toml +++ b/packages/injective-testing/Cargo.toml @@ -5,16 +5,19 @@ edition = "2021" license = "Apache-2.0" name = "injective-testing" repository = "https://github.com/InjectiveLabs/cw-injective/tree/dev/packages/injective-testing" -version = "0.2.1" +version = "1.0.0" [dependencies] -anyhow = { workspace = true } -base64 = { workspace = true } -cosmwasm-std = { workspace = true } -cw-multi-test = { workspace = true } -injective-cosmwasm = { workspace = true, path = "../injective-cosmwasm" } -injective-math = { workspace = true, path = "../injective-math" } -rand = { workspace = true } -secp256k1 = { workspace = true } -serde = { workspace = true } -tiny-keccak = { workspace = true } +anyhow = { workspace = true } +base64 = { workspace = true } +cosmwasm-std = { workspace = true } +cw-multi-test = { workspace = true } +injective-cosmwasm = { workspace = true, path = "../injective-cosmwasm" } +injective-math = { workspace = true, path = "../injective-math" } +injective-std = { workspace = true, path = "../injective-std" } +injective-test-tube = { workspace = true } +prost = { workspace = true } +rand = { workspace = true } +secp256k1 = { workspace = true } +serde = { workspace = true } +tiny-keccak = { workspace = true } diff --git a/packages/injective-testing/src/lib.rs b/packages/injective-testing/src/lib.rs index db595c7e..2d0330a8 100644 --- a/packages/injective-testing/src/lib.rs +++ b/packages/injective-testing/src/lib.rs @@ -1,7 +1,9 @@ -mod address_generator; -mod chain_mock; +mod mocks; +mod multi_test; +mod test_tube; pub mod utils; -pub use address_generator::{generate_inj_address, InjectiveAddressGenerator, StorageAwareInjectiveAddressGenerator}; -pub use chain_mock::*; +pub use mocks::*; +pub use multi_test::*; +pub use test_tube::*; pub use utils::*; diff --git a/packages/injective-testing/src/mocks.rs b/packages/injective-testing/src/mocks.rs new file mode 100644 index 00000000..acd84732 --- /dev/null +++ b/packages/injective-testing/src/mocks.rs @@ -0,0 +1,64 @@ +use crate::utils::human_to_dec; + +use injective_cosmwasm::{DerivativeMarket, MarketId, MarketMidPriceAndTOBResponse, MarketStatus, OracleType, SpotMarket}; +use injective_math::FPDecimal; + +pub const MOCKED_MARKET_ID: &str = "0x01edfab47f124748dc89998eb33144af734484ba07099014594321729a0ca16b"; +pub const MOCKED_SUBACCOUNT_ID: &str = "0x427aee334987c52fa7b567b2662bdbb68614e48c000000000000000000000001"; +pub const MOCKED_FEE_RECIPIENT: &str = "0x01edfab47f124748dc89998eb33144af734484ba07099014594321729a0ca16b"; + +pub const MOCK_EXCHANGE_DECIMALS: i32 = 18i32; +pub const MOCK_BASE_DECIMALS: i32 = 18i32; +pub const MOCK_ATOM_DECIMALS: i32 = 8i32; +pub const MOCK_QUOTE_DECIMALS: i32 = 6i32; + +pub const MOCK_ATOM_DENOM: &str = "atom"; +pub const MOCK_GAS_DENOM: &str = "inj"; +pub const MOCK_BASE_DENOM: &str = "ubase"; +pub const MOCK_QUOTE_DENOM: &str = "usdt"; +pub const MOCK_USDC_DENOM: &str = "usdc"; + +// Mock INJ Market +pub fn mock_spot_market(market_id: &str) -> SpotMarket { + SpotMarket { + ticker: String::from("INJ:USDT"), + base_denom: String::from("inj"), + quote_denom: String::from("usdt"), + market_id: MarketId::unchecked(market_id), + maker_fee_rate: FPDecimal::ZERO, + taker_fee_rate: FPDecimal::ZERO, + status: MarketStatus::Active, + min_price_tick_size: FPDecimal::must_from_str("0.000000000000001000"), + min_quantity_tick_size: FPDecimal::must_from_str("10000000000000.0"), // 0.00001 @ 18dp + relayer_fee_share_rate: FPDecimal::must_from_str("0.4"), + } +} + +// Mock INJ Market +pub fn mock_derivative_market(market_id: &str) -> DerivativeMarket { + DerivativeMarket { + ticker: String::from("INJ:USDT"), + oracle_base: String::from("inj"), + oracle_quote: String::from("usdt"), + oracle_type: OracleType::PriceFeed, + oracle_scale_factor: 0u32, + quote_denom: String::from("usdt"), + market_id: MarketId::unchecked(market_id), + initial_margin_ratio: FPDecimal::must_from_str("0.195"), + maintenance_margin_ratio: FPDecimal::must_from_str("0.05"), + maker_fee_rate: FPDecimal::ZERO, + taker_fee_rate: FPDecimal::ZERO, + isPerpetual: true, + status: MarketStatus::Active, + min_price_tick_size: FPDecimal::must_from_str("1000.0"), // 0.001 + min_quantity_tick_size: FPDecimal::must_from_str("0.001"), // 0.001 + } +} + +pub fn mock_mid_price_tob() -> MarketMidPriceAndTOBResponse { + MarketMidPriceAndTOBResponse { + mid_price: Some(human_to_dec("10.0", MOCK_QUOTE_DECIMALS - MOCK_BASE_DECIMALS)), + best_buy_price: Some(human_to_dec("9.95", MOCK_QUOTE_DECIMALS - MOCK_BASE_DECIMALS)), + best_sell_price: Some(human_to_dec("10.05", MOCK_QUOTE_DECIMALS - MOCK_BASE_DECIMALS)), + } +} diff --git a/packages/injective-testing/src/address_generator.rs b/packages/injective-testing/src/multi_test/address_generator.rs similarity index 100% rename from packages/injective-testing/src/address_generator.rs rename to packages/injective-testing/src/multi_test/address_generator.rs diff --git a/packages/injective-testing/src/chain_mock.rs b/packages/injective-testing/src/multi_test/chain_mock.rs similarity index 100% rename from packages/injective-testing/src/chain_mock.rs rename to packages/injective-testing/src/multi_test/chain_mock.rs diff --git a/packages/injective-testing/src/multi_test/mod.rs b/packages/injective-testing/src/multi_test/mod.rs new file mode 100644 index 00000000..701f0b5d --- /dev/null +++ b/packages/injective-testing/src/multi_test/mod.rs @@ -0,0 +1,5 @@ +mod address_generator; +mod chain_mock; + +pub use address_generator::{generate_inj_address, InjectiveAddressGenerator, StorageAwareInjectiveAddressGenerator}; +pub use chain_mock::*; diff --git a/packages/injective-testing/src/test_tube/authz.rs b/packages/injective-testing/src/test_tube/authz.rs new file mode 100644 index 00000000..10df2c6d --- /dev/null +++ b/packages/injective-testing/src/test_tube/authz.rs @@ -0,0 +1,87 @@ +use injective_std::{ + shim::{Any, Timestamp}, + types::cosmos::{ + authz::v1beta1::{GenericAuthorization, Grant, MsgGrant, MsgRevoke, MsgRevokeResponse}, + bank::v1beta1::SendAuthorization, + base::v1beta1::Coin as BaseCoin, + }, +}; +use injective_test_tube::{Account, Authz, ExecuteResponse, InjectiveTestApp, Module, Runner, SigningAccount}; +use prost::Message; + +pub fn create_generic_authorization(app: &InjectiveTestApp, granter: &SigningAccount, grantee: String, msg: String, expiration: Option) { + let authz = Authz::new(app); + + let mut buf = vec![]; + GenericAuthorization::encode(&GenericAuthorization { msg }, &mut buf).unwrap(); + + authz + .grant( + MsgGrant { + granter: granter.address(), + grantee, + grant: Some(Grant { + authorization: Some(Any { + type_url: GenericAuthorization::TYPE_URL.to_string(), + value: buf.clone(), + }), + expiration, + }), + }, + granter, + ) + .unwrap(); +} + +pub fn revoke_authorization(app: &InjectiveTestApp, granter: &SigningAccount, grantee: String, msg_type_url: String) { + let _res: ExecuteResponse = app + .execute_multiple( + &[( + MsgRevoke { + granter: granter.address(), + grantee, + msg_type_url, + }, + MsgRevoke::TYPE_URL, + )], + granter, + ) + .unwrap(); +} + +pub fn create_send_authorization(app: &InjectiveTestApp, granter: &SigningAccount, grantee: String, amount: BaseCoin, expiration: Option) { + let authz = Authz::new(app); + + let mut buf = vec![]; + SendAuthorization::encode( + &SendAuthorization { + spend_limit: vec![amount], + allow_list: vec![], + }, + &mut buf, + ) + .unwrap(); + + authz + .grant( + MsgGrant { + granter: granter.address(), + grantee, + grant: Some(Grant { + authorization: Some(Any { + type_url: SendAuthorization::TYPE_URL.to_string(), + value: buf.clone(), + }), + expiration, + }), + }, + granter, + ) + .unwrap(); +} + +pub fn execute_grid_authorizations(app: &InjectiveTestApp, granter: &SigningAccount, grantee: String, msgs: Vec) { + for msg in msgs { + create_generic_authorization(app, granter, grantee.clone(), msg, None); + } +} diff --git a/packages/injective-testing/src/test_tube/bank.rs b/packages/injective-testing/src/test_tube/bank.rs new file mode 100644 index 00000000..9038024d --- /dev/null +++ b/packages/injective-testing/src/test_tube/bank.rs @@ -0,0 +1,31 @@ +use cosmwasm_std::Uint128; +use injective_std::types::cosmos::{ + bank::v1beta1::{MsgSend, QueryBalanceRequest}, + base::v1beta1::Coin, +}; +use injective_test_tube::{Account, Bank, InjectiveTestApp, SigningAccount}; +use std::str::FromStr; + +pub fn send(bank: &Bank, amount: &str, denom: &str, from: &SigningAccount, to: &SigningAccount) { + bank.send( + MsgSend { + from_address: from.address(), + to_address: to.address(), + amount: vec![Coin { + amount: amount.to_string(), + denom: denom.to_string(), + }], + }, + from, + ) + .unwrap(); +} + +pub fn query_balance(bank: &Bank, address: String, denom: String) -> Uint128 { + let response = bank.query_balance(&QueryBalanceRequest { address, denom }).unwrap(); + + match response.balance { + Some(balance) => Uint128::from_str(&balance.amount).unwrap(), + None => Uint128::zero(), + } +} diff --git a/packages/injective-testing/src/test_tube/exchange.rs b/packages/injective-testing/src/test_tube/exchange.rs new file mode 100644 index 00000000..2c2b15bb --- /dev/null +++ b/packages/injective-testing/src/test_tube/exchange.rs @@ -0,0 +1,763 @@ +use crate::{ + mocks::{ + MOCK_ATOM_DECIMALS, MOCK_ATOM_DENOM, MOCK_BASE_DECIMALS, MOCK_BASE_DENOM, MOCK_GAS_DENOM, MOCK_QUOTE_DECIMALS, MOCK_QUOTE_DENOM, + MOCK_USDC_DENOM, + }, + utils::{dec_to_proto, scale_price_quantity_perp_market, scale_price_quantity_spot_market, str_coin}, +}; + +use cosmwasm_std::Addr; +use injective_cosmwasm::{get_default_subaccount_id_for_checked_address, SubaccountId}; +use injective_math::FPDecimal; +use injective_std::{ + shim::Any, + types::{ + cosmos::{ + base::v1beta1::Coin as BaseCoin, + gov::v1::{MsgSubmitProposal, MsgVote}, + }, + injective::exchange::v1beta1::{ + DerivativeOrder, MsgBatchUpdateOrders, MsgBatchUpdateOrdersResponse, MsgCancelDerivativeOrder, MsgCreateDerivativeLimitOrder, + MsgCreateDerivativeLimitOrderResponse, MsgCreateSpotLimitOrder, MsgInstantPerpetualMarketLaunch, MsgInstantSpotMarketLaunch, + MsgUpdateParams, OrderInfo, OrderType, PerpetualMarketFunding, Position, QueryDerivativeMarketsRequest, QueryExchangeParamsRequest, + QueryExchangeParamsResponse, QuerySpotMarketsRequest, SpotOrder, + }, + }, +}; +use injective_test_tube::{Account, Exchange, Gov, InjectiveTestApp, Module, Runner, SigningAccount}; +use prost::Message; +use std::str::FromStr; + +pub fn add_exchange_admin(app: &InjectiveTestApp, validator: &SigningAccount, admin_address: String) { + let gov = Gov::new(app); + + let res: QueryExchangeParamsResponse = app + .query("/injective.exchange.v1beta1.Query/QueryExchangeParams", &QueryExchangeParamsRequest {}) + .unwrap(); + + let mut exchange_params = res.params.unwrap(); + exchange_params.exchange_admins.push(admin_address); + exchange_params.max_derivative_order_side_count = 300u32; + + let governance_module_address = "inj10d07y265gmmuvt4z0w9aw880jnsr700jstypyt"; + + let mut buf = vec![]; + MsgUpdateParams::encode( + &MsgUpdateParams { + authority: governance_module_address.to_string(), + params: Some(exchange_params), + }, + &mut buf, + ) + .unwrap(); + + let res = gov + .submit_proposal( + MsgSubmitProposal { + messages: vec![Any { + type_url: MsgUpdateParams::TYPE_URL.to_string(), + value: buf, + }], + initial_deposit: vec![BaseCoin { + amount: "100000000000000000000".to_string(), + denom: "inj".to_string(), + }], + proposer: validator.address(), + metadata: "".to_string(), + title: "Update params".to_string(), + summary: "Basically updating the params".to_string(), + expedited: false, + }, + validator, + ) + .unwrap(); + + let proposal_id = res.events.iter().find(|e| e.ty == "submit_proposal").unwrap().attributes[0].value.clone(); + + gov.vote( + MsgVote { + proposal_id: u64::from_str(&proposal_id).unwrap(), + voter: validator.address(), + option: 1i32, + metadata: "".to_string(), + }, + validator, + ) + .unwrap(); +} + +pub fn create_perp_mid_price(app: &InjectiveTestApp, market_id: &str, base_price: &str, base_quantity: &str, base_margin: &str, spread: f64) { + // Calculate adjusted prices for buy and sell based on the spread + let sell_price = format!("{:.1}", base_price.parse::().unwrap() + spread); + let buy_price = format!("{:.1}", base_price.parse::().unwrap() - spread); + + // Scaling and executing the sell order + let (price, quantity, margin) = scale_price_quantity_perp_market(&sell_price, base_quantity, base_margin, &MOCK_QUOTE_DECIMALS); + execute_derivative_limit_order(app, market_id.to_string(), price, quantity, margin, OrderType::Sell); + + // Scaling and executing the buy order + let (price, quantity, margin) = scale_price_quantity_perp_market(&buy_price, base_quantity, base_margin, &MOCK_QUOTE_DECIMALS); + execute_derivative_limit_order(app, market_id.to_string(), price, quantity, margin, OrderType::Buy); +} + +pub fn create_perp_mid_price_as( + app: &InjectiveTestApp, + market_id: &str, + base_price: &str, + base_quantity: &str, + base_margin: &str, + spread: f64, + trader: &SigningAccount, + subaccount_id: &SubaccountId, +) { + // Calculate adjusted prices for buy and sell based on the spread + let sell_price = format!("{}", base_price.parse::().unwrap() + spread); + let buy_price = format!("{}", base_price.parse::().unwrap() - spread); + + // Scaling and executing the sell order + let (price, quantity, margin) = scale_price_quantity_perp_market(&sell_price, base_quantity, base_margin, &MOCK_QUOTE_DECIMALS); + execute_derivative_limit_order_as( + app, + market_id.to_string(), + price, + quantity, + margin, + OrderType::Sell, + trader, + subaccount_id, + ); + + // Scaling and executing the buy order + let (price, quantity, margin) = scale_price_quantity_perp_market(&buy_price, base_quantity, base_margin, &MOCK_QUOTE_DECIMALS); + execute_derivative_limit_order_as(app, market_id.to_string(), price, quantity, margin, OrderType::Buy, trader, subaccount_id); +} + +pub fn create_spot_mid_price( + app: &InjectiveTestApp, + market_id: &str, + base_price: &str, + base_quantity: &str, + base_decimals: &i32, + quote_decimals: &i32, + spread: f64, +) { + // Calculate adjusted prices for buy and sell based on the spread + let sell_price = format!("{}", base_price.parse::().unwrap() + spread); + let buy_price = format!("{}", base_price.parse::().unwrap() - spread); + + // Scaling and executing the sell order + let (price, quantity) = scale_price_quantity_spot_market(&sell_price, base_quantity, base_decimals, quote_decimals); + execute_spot_limit_order(app, market_id.to_string(), price, quantity, OrderType::Sell); + + // Scaling and executing the buy order + let (price, quantity) = scale_price_quantity_spot_market(&buy_price, base_quantity, base_decimals, quote_decimals); + execute_spot_limit_order(app, market_id.to_string(), price, quantity, OrderType::Buy); +} + +pub fn create_spot_mid_price_as( + app: &InjectiveTestApp, + market_id: &str, + base_price: &str, + base_quantity: &str, + base_decimals: &i32, + quote_decimals: &i32, + spread: f64, + trader: &SigningAccount, + subaccount_id: &SubaccountId, +) { + // Calculate adjusted prices for buy and sell based on the spread + let sell_price = format!("{:.1}", base_price.parse::().unwrap() + spread); + let buy_price = format!("{:.1}", base_price.parse::().unwrap() - spread); + + // Scaling and executing the sell order + let (price, quantity) = scale_price_quantity_spot_market(&sell_price, base_quantity, base_decimals, quote_decimals); + execute_spot_limit_order_as(app, market_id.to_string(), price, quantity, OrderType::Sell, trader, subaccount_id); + + // Scaling and executing the buy order + let (price, quantity) = scale_price_quantity_spot_market(&buy_price, base_quantity, base_decimals, quote_decimals); + execute_spot_limit_order_as(app, market_id.to_string(), price, quantity, OrderType::Buy, trader, subaccount_id); +} + +pub fn create_price_perp( + app: &InjectiveTestApp, + market_id: &str, + base_price: &str, + base_quantity: &str, + base_margin: &str, + spread: f64, + trader: &SigningAccount, + subaccount_id: &SubaccountId, +) -> MsgBatchUpdateOrdersResponse { + let exchange = Exchange::new(app); + + // Calculate adjusted prices for buy and sell based on the spread + let sell_price = format!("{}", base_price.parse::().unwrap() + spread); + let buy_price = format!("{}", base_price.parse::().unwrap() - spread); + + // Scaling and executing the sell order + let (sell_price, sell_quantity, sell_margin) = scale_price_quantity_perp_market(&sell_price, base_quantity, base_margin, &MOCK_QUOTE_DECIMALS); + + // Scaling and executing the buy order + let (buy_price, buy_quantity, buy_margin) = scale_price_quantity_perp_market(&buy_price, base_quantity, base_margin, &MOCK_QUOTE_DECIMALS); + + exchange + .batch_update_orders( + MsgBatchUpdateOrders { + sender: trader.address(), + subaccount_id: subaccount_id.as_str().to_string(), + derivative_market_ids_to_cancel_all: vec![market_id.to_string()], + derivative_orders_to_create: vec![ + DerivativeOrder { + market_id: market_id.to_string(), + order_info: Some(OrderInfo { + subaccount_id: subaccount_id.as_str().to_string(), + fee_recipient: trader.address(), + price: buy_price.clone(), + quantity: buy_quantity.clone(), + cid: "".to_string(), + }), + margin: buy_margin.clone(), + order_type: OrderType::Buy.into(), + trigger_price: "".to_string(), + }, + DerivativeOrder { + market_id: market_id.to_string(), + order_info: Some(OrderInfo { + subaccount_id: subaccount_id.as_str().to_string(), + fee_recipient: trader.address(), + price: sell_price.clone(), + quantity: sell_quantity.clone(), + cid: "".to_string(), + }), + margin: sell_margin.clone(), + order_type: OrderType::Sell.into(), + trigger_price: "".to_string(), + }, + ], + ..Default::default() + }, + trader, + ) + .unwrap() + .data +} + +pub fn create_price_spot( + app: &InjectiveTestApp, + market_id: &str, + base_price: &str, + base_quantity: &str, + spread: f64, + trader: &SigningAccount, + subaccount_id: &SubaccountId, +) -> MsgBatchUpdateOrdersResponse { + let exchange = Exchange::new(app); + + // Calculate adjusted prices for buy and sell based on the spread + let sell_price = format!("{}", base_price.parse::().unwrap() + spread); + let buy_price = format!("{}", base_price.parse::().unwrap() - spread); + + // Scaling and executing the sell order + let (sell_price, sell_quantity) = scale_price_quantity_spot_market(&sell_price, base_quantity, &MOCK_BASE_DECIMALS, &MOCK_QUOTE_DECIMALS); + + // Scaling and executing the buy order + let (buy_price, buy_quantity) = scale_price_quantity_spot_market(&buy_price, base_quantity, &MOCK_BASE_DECIMALS, &MOCK_QUOTE_DECIMALS); + + exchange + .batch_update_orders( + MsgBatchUpdateOrders { + sender: trader.address(), + subaccount_id: subaccount_id.as_str().to_string(), + spot_market_ids_to_cancel_all: vec![market_id.to_string()], + spot_orders_to_create: vec![ + SpotOrder { + market_id: market_id.to_string(), + order_info: Some(OrderInfo { + subaccount_id: subaccount_id.as_str().to_string(), + fee_recipient: trader.address(), + price: buy_price.clone(), + quantity: buy_quantity.clone(), + cid: "".to_string(), + }), + order_type: OrderType::Buy.into(), + trigger_price: "".to_string(), + }, + SpotOrder { + market_id: market_id.to_string(), + order_info: Some(OrderInfo { + subaccount_id: subaccount_id.as_str().to_string(), + fee_recipient: trader.address(), + price: sell_price.clone(), + quantity: sell_quantity.clone(), + cid: "".to_string(), + }), + order_type: OrderType::Sell.into(), + trigger_price: "".to_string(), + }, + ], + ..Default::default() + }, + trader, + ) + .unwrap() + .data +} + +pub fn create_price_spot_and_perp_market( + app: &InjectiveTestApp, + perp_market_id: &str, + spot_market_id: &str, + base_price: &str, + base_quantity: &str, + base_margin: &str, + spread: f64, + trader: &SigningAccount, + subaccount_id: &SubaccountId, +) -> MsgBatchUpdateOrdersResponse { + let exchange = Exchange::new(app); + + // Calculate adjusted prices for buy and sell based on the spread + let sell_price = format!("{}", base_price.parse::().unwrap() + spread); + let buy_price = format!("{}", base_price.parse::().unwrap() - spread); + + // Scaling and executing the sell order + let (perp_sell_price, perp_sell_quantity, perp_sell_margin) = + scale_price_quantity_perp_market(&sell_price, base_quantity, base_margin, &MOCK_QUOTE_DECIMALS); + + // Scaling and executing the buy order + let (perp_buy_price, perp_buy_quantity, perp_buy_margin) = + scale_price_quantity_perp_market(&buy_price, base_quantity, base_margin, &MOCK_QUOTE_DECIMALS); + + // Scaling and executing the sell order + let (spot_sell_price, spot_sell_quantity) = + scale_price_quantity_spot_market(&sell_price, base_quantity, &MOCK_BASE_DECIMALS, &MOCK_QUOTE_DECIMALS); + + // Scaling and executing the buy order + let (spot_buy_price, spot_buy_quantity) = scale_price_quantity_spot_market(&buy_price, base_quantity, &MOCK_BASE_DECIMALS, &MOCK_QUOTE_DECIMALS); + + exchange + .batch_update_orders( + MsgBatchUpdateOrders { + sender: trader.address(), + subaccount_id: subaccount_id.as_str().to_string(), + derivative_market_ids_to_cancel_all: vec![perp_market_id.to_string()], + spot_market_ids_to_cancel_all: vec![spot_market_id.to_string()], + derivative_orders_to_create: vec![ + DerivativeOrder { + market_id: perp_market_id.to_string(), + order_info: Some(OrderInfo { + subaccount_id: subaccount_id.as_str().to_string(), + fee_recipient: trader.address(), + price: perp_buy_price.clone(), + quantity: perp_buy_quantity.clone(), + cid: "".to_string(), + }), + margin: perp_buy_margin.clone(), + order_type: OrderType::Buy.into(), + trigger_price: "".to_string(), + }, + DerivativeOrder { + market_id: perp_market_id.to_string(), + order_info: Some(OrderInfo { + subaccount_id: subaccount_id.as_str().to_string(), + fee_recipient: trader.address(), + price: perp_sell_price.clone(), + quantity: perp_sell_quantity.clone(), + cid: "".to_string(), + }), + margin: perp_sell_margin.clone(), + order_type: OrderType::Sell.into(), + trigger_price: "".to_string(), + }, + ], + spot_orders_to_create: vec![ + SpotOrder { + market_id: spot_market_id.to_string(), + order_info: Some(OrderInfo { + subaccount_id: subaccount_id.as_str().to_string(), + fee_recipient: trader.address(), + price: spot_buy_price.clone(), + quantity: spot_buy_quantity.clone(), + cid: "".to_string(), + }), + order_type: OrderType::Buy.into(), + trigger_price: "".to_string(), + }, + SpotOrder { + market_id: spot_market_id.to_string(), + order_info: Some(OrderInfo { + subaccount_id: subaccount_id.as_str().to_string(), + fee_recipient: trader.address(), + price: spot_sell_price.clone(), + quantity: spot_sell_quantity.clone(), + cid: "".to_string(), + }), + order_type: OrderType::Sell.into(), + trigger_price: "".to_string(), + }, + ], + ..Default::default() + }, + trader, + ) + .unwrap() + .data +} + +pub fn cancel_derivative_order_as( + app: &InjectiveTestApp, + market_id: String, + order_hash: String, + trader: &SigningAccount, + subaccount_id: &SubaccountId, +) { + let exchange = Exchange::new(app); + + exchange + .cancel_derivative_order( + MsgCancelDerivativeOrder { + sender: trader.address(), + market_id, + subaccount_id: subaccount_id.as_str().to_string(), + order_hash, + order_mask: 0i32, + cid: "".to_string(), + }, + trader, + ) + .unwrap(); +} + +pub fn launch_spot_market(exchange: &Exchange, signer: &SigningAccount, ticker: String) -> String { + exchange + .instant_spot_market_launch( + MsgInstantSpotMarketLaunch { + sender: signer.address(), + ticker: ticker.clone(), + base_denom: MOCK_BASE_DECIMALS.to_string(), + quote_denom: MOCK_QUOTE_DECIMALS.to_string(), + min_price_tick_size: dec_to_proto(FPDecimal::must_from_str("0.000000000000001")), + min_quantity_tick_size: dec_to_proto(FPDecimal::must_from_str("1000000000000000")), + min_notional: dec_to_proto(FPDecimal::must_from_str("1")), + }, + signer, + ) + .unwrap(); + + get_spot_market_id(exchange, ticker) +} + +pub fn launch_spot_market_atom(exchange: &Exchange, signer: &SigningAccount, ticker: String) -> String { + exchange + .instant_spot_market_launch( + MsgInstantSpotMarketLaunch { + sender: signer.address(), + ticker: ticker.clone(), + base_denom: MOCK_ATOM_DECIMALS.to_string(), + quote_denom: MOCK_QUOTE_DECIMALS.to_string(), + min_price_tick_size: dec_to_proto(FPDecimal::must_from_str("0.000010000000000000")), + min_quantity_tick_size: dec_to_proto(FPDecimal::must_from_str("100000")), + min_notional: dec_to_proto(FPDecimal::must_from_str("1")), + }, + signer, + ) + .unwrap(); + + get_spot_market_id(exchange, ticker) +} + +pub fn launch_spot_market_custom( + exchange: &Exchange, + signer: &SigningAccount, + ticker: String, + base_denom: String, + quote_denom: String, + min_price_tick_size: String, + min_quantity_tick_size: String, +) -> String { + exchange + .instant_spot_market_launch( + MsgInstantSpotMarketLaunch { + sender: signer.address(), + ticker: ticker.clone(), + base_denom, + quote_denom, + min_price_tick_size: dec_to_proto(FPDecimal::must_from_str(&min_price_tick_size)), + min_quantity_tick_size: dec_to_proto(FPDecimal::must_from_str(&min_quantity_tick_size)), + min_notional: dec_to_proto(FPDecimal::must_from_str("1")), + }, + signer, + ) + .unwrap(); + + get_spot_market_id(exchange, ticker) +} + +pub fn launch_perp_market(exchange: &Exchange, signer: &SigningAccount, ticker: String) -> String { + exchange + .instant_perpetual_market_launch( + MsgInstantPerpetualMarketLaunch { + sender: signer.address(), + ticker: ticker.to_owned(), + quote_denom: "usdt".to_string(), + oracle_base: "inj".to_string(), + oracle_quote: "usdt".to_string(), + oracle_scale_factor: 6u32, + oracle_type: 2i32, + maker_fee_rate: "-000100000000000000".to_owned(), + taker_fee_rate: "0005000000000000000".to_owned(), + initial_margin_ratio: "195000000000000000".to_owned(), + maintenance_margin_ratio: "50000000000000000".to_owned(), + min_price_tick_size: "1000000000000000000000".to_owned(), + min_quantity_tick_size: "1000000000000000".to_owned(), + min_notional: dec_to_proto(FPDecimal::must_from_str("1")), + }, + signer, + ) + .unwrap(); + + get_perpetual_market_id(exchange, ticker) +} + +pub fn launch_perp_market_atom(exchange: &Exchange, signer: &SigningAccount, ticker: String) -> String { + exchange + .instant_perpetual_market_launch( + MsgInstantPerpetualMarketLaunch { + sender: signer.address(), + ticker: ticker.to_owned(), + quote_denom: "usdt".to_string(), + oracle_base: "atom".to_string(), + oracle_quote: "usdt".to_string(), + oracle_scale_factor: 6u32, + oracle_type: 2i32, + maker_fee_rate: "-000100000000000000".to_owned(), + taker_fee_rate: "0005000000000000000".to_owned(), + initial_margin_ratio: "195000000000000000".to_owned(), + maintenance_margin_ratio: "50000000000000000".to_owned(), + min_price_tick_size: "1000000000000000000000".to_owned(), + min_quantity_tick_size: "10000000000000000".to_owned(), + min_notional: dec_to_proto(FPDecimal::must_from_str("1")), + }, + signer, + ) + .unwrap(); + + get_perpetual_market_id(exchange, ticker) +} + +pub fn execute_spot_limit_order(app: &InjectiveTestApp, market_id: String, price: String, quantity: String, order_type: OrderType) { + let trader = app + .init_account(&[ + str_coin("1000000", MOCK_ATOM_DENOM, MOCK_ATOM_DECIMALS), + str_coin("1000000", MOCK_GAS_DENOM, MOCK_BASE_DECIMALS), + str_coin("1000000", MOCK_BASE_DENOM, MOCK_BASE_DECIMALS), + str_coin("1000000", MOCK_QUOTE_DENOM, MOCK_QUOTE_DECIMALS), + str_coin("1000000", MOCK_USDC_DENOM, MOCK_QUOTE_DECIMALS), + ]) + .unwrap(); + + let exchange = Exchange::new(app); + + exchange + .create_spot_limit_order( + MsgCreateSpotLimitOrder { + sender: trader.address(), + order: Some(SpotOrder { + market_id, + order_info: Some(OrderInfo { + subaccount_id: get_default_subaccount_id_for_checked_address(&Addr::unchecked(trader.address())) + .as_str() + .to_string(), + fee_recipient: trader.address(), + price, + quantity, + cid: "".to_string(), + }), + order_type: order_type.into(), + trigger_price: "".to_string(), + }), + }, + &trader, + ) + .unwrap(); +} + +pub fn execute_spot_limit_order_as( + app: &InjectiveTestApp, + market_id: String, + price: String, + quantity: String, + order_type: OrderType, + trader: &SigningAccount, + subaccount_id: &SubaccountId, +) { + let exchange = Exchange::new(app); + + exchange + .create_spot_limit_order( + MsgCreateSpotLimitOrder { + sender: trader.address(), + order: Some(SpotOrder { + market_id, + order_info: Some(OrderInfo { + subaccount_id: subaccount_id.as_str().to_string(), + fee_recipient: trader.address(), + price, + quantity, + cid: "".to_string(), + }), + order_type: order_type.into(), + trigger_price: "".to_string(), + }), + }, + trader, + ) + .unwrap(); +} + +pub fn estimate_funding_apy(funding_info: &PerpetualMarketFunding, position: &Position) -> FPDecimal { + let cumulative_funding = FPDecimal::from_str(&funding_info.cumulative_funding).unwrap(); + let cumulative_funding_entry = FPDecimal::from_str(&position.cumulative_funding_entry).unwrap(); + + cumulative_funding - cumulative_funding_entry +} + +pub fn execute_derivative_limit_order( + app: &InjectiveTestApp, + market_id: String, + price: String, + quantity: String, + margin: String, + order_type: OrderType, +) { + let trader = app + .init_account(&[ + str_coin("1000000", MOCK_ATOM_DENOM, MOCK_ATOM_DECIMALS), + str_coin("1000000", MOCK_BASE_DENOM, MOCK_BASE_DECIMALS), + str_coin("1000000", MOCK_GAS_DENOM, MOCK_BASE_DECIMALS), + str_coin("1000000", MOCK_QUOTE_DENOM, MOCK_QUOTE_DECIMALS), + ]) + .unwrap(); + + let exchange = Exchange::new(app); + + exchange + .create_derivative_limit_order( + MsgCreateDerivativeLimitOrder { + sender: trader.address(), + order: Some(DerivativeOrder { + market_id, + order_info: Some(OrderInfo { + subaccount_id: get_default_subaccount_id_for_checked_address(&Addr::unchecked(trader.address())) + .as_str() + .to_string(), + fee_recipient: trader.address(), + price, + quantity, + cid: "".to_string(), + }), + margin, + order_type: order_type.into(), + trigger_price: "".to_string(), + }), + }, + &trader, + ) + .unwrap(); +} + +pub fn execute_derivative_limit_order_as( + app: &InjectiveTestApp, + market_id: String, + price: String, + quantity: String, + margin: String, + order_type: OrderType, + trader: &SigningAccount, + subaccount_id: &SubaccountId, +) -> MsgCreateDerivativeLimitOrderResponse { + let exchange = Exchange::new(app); + + exchange + .create_derivative_limit_order( + MsgCreateDerivativeLimitOrder { + sender: trader.address(), + order: Some(DerivativeOrder { + market_id, + order_info: Some(OrderInfo { + subaccount_id: subaccount_id.as_str().to_string(), + fee_recipient: trader.address(), + price, + quantity, + cid: "".to_string(), + }), + margin, + order_type: order_type.into(), + trigger_price: "".to_string(), + }), + }, + trader, + ) + .unwrap() + .data +} + +pub fn remove_orders( + app: &InjectiveTestApp, + perp_market_id: &str, + spot_market_id: &str, + trader: &SigningAccount, + subaccount_id: &SubaccountId, +) -> MsgBatchUpdateOrdersResponse { + let exchange = Exchange::new(app); + + exchange + .batch_update_orders( + MsgBatchUpdateOrders { + sender: trader.address(), + subaccount_id: subaccount_id.as_str().to_string(), + derivative_market_ids_to_cancel_all: vec![perp_market_id.to_string()], + spot_market_ids_to_cancel_all: vec![spot_market_id.to_string()], + ..Default::default() + }, + trader, + ) + .unwrap() + .data +} + +pub fn get_spot_market_id(exchange: &Exchange, ticker: String) -> String { + let spot_markets = exchange + .query_spot_markets(&QuerySpotMarketsRequest { + status: "Active".to_string(), + market_ids: vec![], + }) + .unwrap() + .markets; + + let market = spot_markets.iter().find(|m| m.ticker == ticker).unwrap(); + + market.market_id.to_string() +} + +pub fn get_perpetual_market_id(exchange: &Exchange, ticker: String) -> String { + let perpetual_markets = exchange + .query_derivative_markets(&QueryDerivativeMarketsRequest { + status: "Active".to_string(), + market_ids: vec![], + with_mid_price_and_tob: false, + }) + .unwrap() + .markets; + + let market = perpetual_markets + .iter() + .filter(|m| m.market.is_some()) + .find(|m| m.market.as_ref().unwrap().ticker == ticker) + .unwrap() + .market + .as_ref() + .unwrap(); + + market.market_id.to_string() +} diff --git a/packages/injective-testing/src/test_tube/insurance.rs b/packages/injective-testing/src/test_tube/insurance.rs new file mode 100644 index 00000000..55c06ea0 --- /dev/null +++ b/packages/injective-testing/src/test_tube/insurance.rs @@ -0,0 +1,38 @@ +use crate::{mocks::MOCK_QUOTE_DECIMALS, utils::human_to_dec}; + +use injective_std::types::{ + cosmos::base::v1beta1::Coin as BaseCoin, + injective::{insurance::v1beta1::MsgCreateInsuranceFund, oracle::v1beta1::OracleType}, +}; +use injective_test_tube::{Account, InjectiveTestApp, Insurance, Module, SigningAccount}; + +pub fn launch_insurance_fund( + app: &InjectiveTestApp, + signer: &SigningAccount, + ticker: &str, + quote: &str, + oracle_base: &str, + oracle_quote: &str, + oracle_type: OracleType, +) { + let insurance = Insurance::new(app); + + insurance + .create_insurance_fund( + MsgCreateInsuranceFund { + sender: signer.address(), + ticker: ticker.to_string(), + quote_denom: quote.to_string(), + oracle_base: oracle_base.to_string(), + oracle_quote: oracle_quote.to_string(), + oracle_type: oracle_type as i32, + expiry: -1i64, + initial_deposit: Some(BaseCoin { + amount: human_to_dec("1_000", MOCK_QUOTE_DECIMALS).to_string(), + denom: quote.to_string(), + }), + }, + signer, + ) + .unwrap(); +} diff --git a/packages/injective-testing/src/test_tube/mod.rs b/packages/injective-testing/src/test_tube/mod.rs new file mode 100644 index 00000000..78255b89 --- /dev/null +++ b/packages/injective-testing/src/test_tube/mod.rs @@ -0,0 +1,13 @@ +mod authz; +mod bank; +mod exchange; +mod insurance; +mod oracle; +mod utils; + +pub use authz::*; +pub use bank::*; +pub use exchange::*; +pub use insurance::*; +pub use oracle::*; +pub use utils::*; diff --git a/packages/injective-testing/src/test_tube/oracle.rs b/packages/injective-testing/src/test_tube/oracle.rs new file mode 100644 index 00000000..b225cef2 --- /dev/null +++ b/packages/injective-testing/src/test_tube/oracle.rs @@ -0,0 +1,112 @@ +use injective_std::{ + shim::Any, + types::{ + cosmos::{ + base::v1beta1::Coin as BaseCoin, + gov::{v1::MsgVote, v1beta1::MsgSubmitProposal as MsgSubmitProposalV1Beta1}, + }, + injective::oracle::v1beta1::{GrantPriceFeederPrivilegeProposal, MsgRelayPriceFeedPrice, QueryOraclePriceRequest, QueryOraclePriceResponse}, + }, +}; +use injective_test_tube::{Account, Gov, InjectiveTestApp, Module, Oracle, SigningAccount}; +use prost::Message; +use std::str::FromStr; + +pub fn launch_price_feed_oracle( + app: &InjectiveTestApp, + signer: &SigningAccount, + validator: &SigningAccount, + base: &str, + quote: &str, + dec_price: String, +) { + let gov = Gov::new(app); + let oracle = Oracle::new(app); + + let mut buf = vec![]; + GrantPriceFeederPrivilegeProposal::encode( + &GrantPriceFeederPrivilegeProposal { + title: "test-proposal".to_string(), + description: "test-proposal".to_string(), + base: base.to_string(), + quote: quote.to_string(), + relayers: vec![signer.address()], + }, + &mut buf, + ) + .unwrap(); + + let res = gov + .submit_proposal_v1beta1( + MsgSubmitProposalV1Beta1 { + content: Some(Any { + type_url: "/injective.oracle.v1beta1.GrantPriceFeederPrivilegeProposal".to_string(), + value: buf, + }), + initial_deposit: vec![BaseCoin { + amount: "100000000000000000000".to_string(), + denom: "inj".to_string(), + }], + proposer: validator.address(), + }, + validator, + ) + .unwrap(); + + let proposal_id = res.events.iter().find(|e| e.ty == "submit_proposal").unwrap().attributes[0] + .value + .to_owned(); + + gov.vote( + MsgVote { + proposal_id: u64::from_str(&proposal_id).unwrap(), + voter: validator.address(), + option: 1i32, + metadata: "".to_string(), + }, + validator, + ) + .unwrap(); + + // NOTE: increase the block time in order to move past the voting period + app.increase_time(10u64); + + oracle + .relay_price_feed( + MsgRelayPriceFeedPrice { + sender: signer.address(), + base: vec![base.to_string()], + quote: vec![quote.to_string()], + price: vec![dec_price], // 1.2@18dp + }, + signer, + ) + .unwrap(); +} + +pub fn relay_price_feed_price(oracle: &Oracle, relayer: &SigningAccount, base_denom: &str, quote_denom: &str, price: &str) { + oracle + .relay_price_feed( + MsgRelayPriceFeedPrice { + sender: relayer.address(), + base: vec![base_denom.to_string()], + quote: vec![quote_denom.to_string()], + price: vec![price.to_string()], + }, + relayer, + ) + .unwrap(); +} + +pub fn query_oracle_mark_price(app: &InjectiveTestApp, base_denom: &str, quote_denom: &str) -> QueryOraclePriceResponse { + let oracle = Oracle::new(app); + + oracle + .query_oracle_price(&QueryOraclePriceRequest { + oracle_type: 2, + base: base_denom.to_string(), + quote: quote_denom.to_string(), + scaling_options: None, + }) + .unwrap() +} diff --git a/packages/injective-testing/src/test_tube/utils.rs b/packages/injective-testing/src/test_tube/utils.rs new file mode 100644 index 00000000..e28ba6aa --- /dev/null +++ b/packages/injective-testing/src/test_tube/utils.rs @@ -0,0 +1,24 @@ +use injective_test_tube::{InjectiveTestApp, SigningAccount, Wasm}; + +pub fn wasm_file(contract_name: String) -> String { + let snaked_name = contract_name.replace('-', "_"); + let arch = std::env::consts::ARCH; + + let target = format!("../../target/wasm32-unknown-unknown/release/{snaked_name}.wasm"); + + let artifacts_dir = std::env::var("ARTIFACTS_DIR_PATH").unwrap_or_else(|_| "artifacts".to_string()); + let arch_target = format!("../../{artifacts_dir}/{snaked_name}-{arch}.wasm"); + + if std::path::Path::new(&target).exists() { + target + } else if std::path::Path::new(&arch_target).exists() { + arch_target + } else { + format!("../../{artifacts_dir}/{snaked_name}.wasm") + } +} + +pub fn store_code(wasm: &Wasm, owner: &SigningAccount, contract_name: String) -> u64 { + let wasm_byte_code = std::fs::read(wasm_file(contract_name)).unwrap(); + wasm.store_code(&wasm_byte_code, None, owner).unwrap().data.code_id +} diff --git a/packages/injective-testing/src/utils.rs b/packages/injective-testing/src/utils.rs index 4f91c5aa..af6e669d 100644 --- a/packages/injective-testing/src/utils.rs +++ b/packages/injective-testing/src/utils.rs @@ -1,20 +1,18 @@ use cosmwasm_std::{coin, Coin}; use injective_math::{scale::Scaled, FPDecimal}; -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -#[repr(i32)] -pub enum Decimals { - Eighteen = 18, - Six = 6, +pub fn assert_execute_error(message: &str) -> String { + format!( + "execute error: failed to execute message; message index: 0: {}: execute wasm contract failed", + message + ) } -impl Decimals { - pub fn get_decimals(&self) -> i32 { - match self { - Decimals::Eighteen => 18, - Decimals::Six => 6, - } - } +pub fn assert_instantiate_error(message: &str) -> String { + format!( + "execute error: failed to execute message; message index: 0: {}: instantiate wasm contract failed", + message + ) } pub fn proto_to_dec(val: &str) -> FPDecimal { @@ -29,8 +27,8 @@ pub fn dec_to_proto(val: FPDecimal) -> String { val.scaled(18).to_string() } -pub fn human_to_dec(raw_number: &str, decimals: Decimals) -> FPDecimal { - FPDecimal::must_from_str(&raw_number.replace('_', "")).scaled(decimals.get_decimals()) +pub fn human_to_dec(raw_number: &str, decimals: i32) -> FPDecimal { + FPDecimal::must_from_str(&raw_number.replace('_', "")).scaled(decimals) } pub fn human_to_i64(raw_number: &str, exponent: i32) -> i64 { @@ -43,8 +41,31 @@ pub fn human_to_proto(raw_number: &str, decimals: i32) -> String { FPDecimal::must_from_str(&raw_number.replace('_', "")).scaled(18 + decimals).to_string() } -pub fn str_coin(human_amount: &str, denom: &str, decimals: Decimals) -> Coin { +pub fn str_coin(human_amount: &str, denom: &str, decimals: i32) -> Coin { let scaled_amount = human_to_dec(human_amount, decimals); let as_int: u128 = scaled_amount.into(); coin(as_int, denom) } + +pub fn scale_price_quantity_spot_market(price: &str, quantity: &str, base_decimals: &i32, quote_decimals: &i32) -> (String, String) { + let price_dec = FPDecimal::must_from_str(price.replace('_', "").as_str()); + let quantity_dec = FPDecimal::must_from_str(quantity.replace('_', "").as_str()); + + let scaled_price = price_dec.scaled(quote_decimals - base_decimals); + let scaled_quantity = quantity_dec.scaled(*base_decimals); + + (dec_to_proto(scaled_price), dec_to_proto(scaled_quantity)) +} + +pub fn scale_price_quantity_perp_market(price: &str, quantity: &str, margin_ratio: &str, quote_decimals: &i32) -> (String, String, String) { + let price_dec = FPDecimal::must_from_str(price.replace('_', "").as_str()); + let quantity_dec = FPDecimal::must_from_str(quantity.replace('_', "").as_str()); + let margin_ratio_dec = FPDecimal::must_from_str(margin_ratio.replace('_', "").as_str()); + + let scaled_price = price_dec.scaled(*quote_decimals); + let scaled_quantity = quantity_dec; + + let scaled_margin = (price_dec * quantity_dec * margin_ratio_dec).scaled(*quote_decimals); + + (dec_to_proto(scaled_price), dec_to_proto(scaled_quantity), dec_to_proto(scaled_margin)) +}