Skip to content
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
aee2d17
add premium field in SetPriceReq and MakerOrder
laruh Mar 11, 2025
1736774
wip
laruh Mar 11, 2025
810addf
comeback to prod ver
laruh Mar 13, 2025
2d996d1
Update Buy and Sell actions, provide more notes
laruh Mar 13, 2025
9eb0663
improve match_with_request Sell; add premium to start state machines
laruh Mar 24, 2025
419d91a
premium in MakerReserved
laruh Mar 24, 2025
4f1002c
use old comment for resulting base amount in Sell legacy
laruh Mar 24, 2025
cd53b6d
use Option for premium, as new_protocol::MakerReserved needs BigRatio…
laruh Mar 25, 2025
1344166
Option premium field in start_swaps test function
laruh Mar 25, 2025
99c7d41
provide taker_method: TakerMethod field to start_swaps fn
laruh Mar 26, 2025
c3217ae
buy and sell work
laruh Mar 27, 2025
af2c3c5
Merge remote-tracking branch 'origin/dev' into premium-tpu
laruh Mar 27, 2025
18dc713
fix utxo buy test; add burnkey_as_alice test when taker sells
laruh Mar 27, 2025
4aaf0bd
fix burnkey_as_alice test for sell
laruh Mar 27, 2025
96594e1
provide test_v2_swap_utxo_utxo_impl_common function for tests
laruh Mar 28, 2025
7d2f22c
Add a clarification doc comment for the premium field
laruh Apr 1, 2025
011798a
review: create is_legacy variable
laruh Apr 1, 2025
e19e1c5
review: move base amount calc to a variable
laruh Apr 2, 2025
385b382
Don't force the taker to send all amount they offered in the Buy action
laruh Apr 4, 2025
285b9ef
review: create premium variable before taker action matching
laruh Apr 9, 2025
74c126d
add safe check for taker real price
laruh Apr 10, 2025
1332c21
review: doc com for premium
laruh Apr 14, 2025
0207b02
merge with origin/dev
shamardy Jul 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 91 additions & 14 deletions mm2src/mm2_main/src/lp_ordermatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1639,7 +1639,22 @@ impl TakerOrder {
|| self.base_orderbook_ticker.as_ref() == Some(&reserved.rel))
&& (self.request.rel == reserved.base
|| self.rel_orderbook_ticker.as_ref() == Some(&reserved.base));
if match_ticker && my_base_amount == other_rel_amount && my_rel_amount <= other_base_amount {

// Reject if any common conditions are unmet
if !match_ticker || my_base_amount != other_rel_amount {
return MatchReservedResult::NotMatched;
}

let other_base_amount = if self.request.swap_version.is_legacy() || reserved.swap_version.is_legacy() {
other_base_amount.clone()
} else {
let premium = &reserved.premium.clone().unwrap_or_default();
let other_price = &(my_base_amount - premium) / other_base_amount;
// In match_with_request function, we allowed maker to send fewer coins for taker sell action
other_base_amount + &(premium / &other_price)
};

if my_rel_amount <= &other_base_amount {
MatchReservedResult::Matched
} else {
MatchReservedResult::NotMatched
Expand Down Expand Up @@ -1722,6 +1737,8 @@ pub struct MakerOrder {
p2p_privkey: Option<SerializableSecp256k1Keypair>,
#[serde(default, skip_serializing_if = "SwapVersion::is_legacy")]
pub swap_version: SwapVersion,
#[serde(default, skip_serializing_if = "Option::is_none")]
premium: Option<MmNumber>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not add a doc comment for this field?

}

pub struct MakerOrderBuilder<'a> {
Expand All @@ -1735,6 +1752,7 @@ pub struct MakerOrderBuilder<'a> {
conf_settings: Option<OrderConfirmationsSettings>,
save_in_history: bool,
swap_version: u8,
premium: Option<MmNumber>,
}

pub enum MakerOrderBuildError {
Expand Down Expand Up @@ -1885,6 +1903,7 @@ impl<'a> MakerOrderBuilder<'a> {
conf_settings: None,
save_in_history: true,
swap_version: SWAP_VERSION_DEFAULT,
premium: Default::default(),
}
}

Expand Down Expand Up @@ -1929,6 +1948,11 @@ impl<'a> MakerOrderBuilder<'a> {
/// In the future alls users will be using TPU V2 by default without "use_trading_proto_v2" configuration.
pub fn set_legacy_swap_v(&mut self) { self.swap_version = legacy_swap_version() }

pub fn with_premium(mut self, premium: Option<MmNumber>) -> Self {
self.premium = premium;
self
}

/// Build MakerOrder
#[allow(clippy::result_large_err)]
pub fn build(self) -> Result<MakerOrder, MakerOrderBuildError> {
Expand Down Expand Up @@ -1986,6 +2010,7 @@ impl<'a> MakerOrderBuilder<'a> {
rel_orderbook_ticker: self.rel_orderbook_ticker,
p2p_privkey,
swap_version: SwapVersion::from(self.swap_version),
premium: self.premium,
})
}

Expand All @@ -2011,6 +2036,7 @@ impl<'a> MakerOrderBuilder<'a> {
rel_orderbook_ticker: None,
p2p_privkey: None,
swap_version: SwapVersion::from(self.swap_version),
premium: Default::default(),
}
}
}
Expand Down Expand Up @@ -2049,31 +2075,66 @@ impl MakerOrder {
return OrderMatchResult::NotMatched;
}

let is_legacy = self.swap_version.is_legacy() || taker.swap_version.is_legacy();

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we may get premium as reference here, like:

        let premium_default = MmNumber::default();
        let premium = self.premium.as_ref().unwrap_or(&premium_default);

and use it within match taker.action {...} instead of cloning.
(but not sure this is much better though)

match taker.action {
TakerAction::Buy => {
let ticker_match = (self.base == taker.base
|| self.base_orderbook_ticker.as_ref() == Some(&taker.base))
&& (self.rel == taker.rel || self.rel_orderbook_ticker.as_ref() == Some(&taker.rel));
// taker_base_amount: the amount taker desires to buy (input.volume from SellBuyRequest)
// taker_rel_amount: the amount taker is willing to pay (input.volume * input.price, where input is SellBuyRequest)
// taker_price: the effective price offered by the taker
let taker_price = taker_rel_amount / taker_base_amount;
if ticker_match
&& taker_base_amount <= &self.available_amount()
&& taker_base_amount >= &self.min_base_vol
&& taker_price >= self.price
{

// Reject if any basic conditions are not satisfied
let base_amount_exceeds = taker_base_amount > &self.available_amount();
let below_min_volume = taker_base_amount < &self.min_base_vol;
let price_too_low = taker_price < self.price;

if !ticker_match || base_amount_exceeds || below_min_volume || price_too_low {
return OrderMatchResult::NotMatched;
}

if is_legacy {
// Legacy mode: use maker's price to calculate rel amount
OrderMatchResult::Matched((taker_base_amount.clone(), taker_base_amount * &self.price))
} else {
OrderMatchResult::NotMatched
// taker_rel_amount must cover the premium requested by maker
let required_rel_amount =
taker_base_amount * &self.price + self.premium.clone().unwrap_or_default();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we require Taker to pay Maker order's premium.
As I can see, premium is a fixed amount in rel coin units, assigned when the order is created.
What if the order is filled only partially, will Taker always pay the same premium amount (that is, shouldn't premium be proportional to the order amount being filled)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the order is filled only partially, will Taker always pay the same premium amount

yes, premium is a fixed rel amount maker requires for a swap. They ask it for the action (swap) not for a trading volume.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not fair:
If an order was filled 3 times partially the maker would receive 3 premiums.
If same order is filled only one time for the full amount the maker receives only 1 premium.
This seems unfair. I think the maker should receive same premium no matter how many times the order was filled.
For this the premium should be calculated proportionally to the amount filled.

(Not sure though, maybe this was discussed already and was decided to pay premium like it is implemented now)

Copy link
Author

@laruh laruh Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During swap maker pays fees for transactions, which doesn't depend on tx value, its better to treat premium as guaranteed fixed amount which could cover maker expenses on swap

if taker_rel_amount >= &required_rel_amount {
// TPU mode: treat buy as a limit order using taker's base and rel amounts
OrderMatchResult::Matched((taker_base_amount.clone(), taker_rel_amount.clone()))
} else {
OrderMatchResult::NotMatched
}
}
},
TakerAction::Sell => {
let ticker_match = (self.base == taker.rel || self.base_orderbook_ticker.as_ref() == Some(&taker.rel))
&& (self.rel == taker.base || self.rel_orderbook_ticker.as_ref() == Some(&taker.base));
let taker_price = taker_base_amount / taker_rel_amount;
let premium = self.premium.clone().unwrap_or_default();

// Calculate the resulting base amount using the Maker's price instead of the Taker's.
let matched_base_amount = taker_base_amount / &self.price;
let matched_rel_amount = taker_base_amount.clone();
// Determine the matched amounts depending on version
let (matched_base_amount, matched_rel_amount) = if is_legacy {
// Legacy: calculate the resulting base amount using the Maker's price instead of the Taker's.
(taker_base_amount / &self.price, taker_base_amount.clone())
} else {
// For TPU, if the total rel amount from the taker (rel is coin which should be sent by taker during swap)
// is less than or equal to the maker's premium, the trade is not possible
if taker_base_amount <= &premium {
return OrderMatchResult::NotMatched;
}
// Calculate the resulting base amount using the maker's price instead of the taker's.
// The maker wants to "take" an additional portion of rel as a premium,
// so we reduce the base amount the maker gives by (premium / price).
let matched_base_amount = &(taker_base_amount - &premium) / &self.price;
(matched_base_amount, taker_base_amount.clone())
};

// Match if all common conditions are met
if ticker_match
&& matched_base_amount <= self.available_amount()
&& matched_base_amount >= self.min_base_vol
Expand Down Expand Up @@ -2142,6 +2203,7 @@ impl From<TakerOrder> for MakerOrder {
rel_orderbook_ticker: taker_order.rel_orderbook_ticker,
p2p_privkey: taker_order.p2p_privkey,
swap_version: taker_order.request.swap_version,
premium: Default::default(),
},
// The "buy" taker order is recreated with reversed pair as Maker order is always considered as "sell"
TakerAction::Buy => {
Expand All @@ -2165,6 +2227,7 @@ impl From<TakerOrder> for MakerOrder {
rel_orderbook_ticker: taker_order.base_orderbook_ticker,
p2p_privkey: taker_order.p2p_privkey,
swap_version: taker_order.request.swap_version,
premium: Default::default(),
}
},
}
Expand Down Expand Up @@ -2217,6 +2280,11 @@ pub struct MakerReserved {
pub rel_protocol_info: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "SwapVersion::is_legacy")]
pub swap_version: SwapVersion,
/// Note: `std::default::Default` is not implemented for `num_rational::Ratio<mm2_number::BigInt>`
/// in the [new_protocol::MakerReserved] structure. As a result, we use `Option<BigRational>` there.
/// It is preferable to follow this same approach in the current structure for consistency.
#[serde(default, skip_serializing_if = "Option::is_none")]
premium: Option<MmNumber>,
}

impl MakerReserved {
Expand Down Expand Up @@ -2245,6 +2313,7 @@ impl MakerReserved {
base_protocol_info: message.base_protocol_info,
rel_protocol_info: message.rel_protocol_info,
swap_version: message.swap_version,
premium: message.premium.map(MmNumber::from),
}
}
}
Expand All @@ -2262,6 +2331,7 @@ impl From<MakerReserved> for new_protocol::OrdermatchMessage {
base_protocol_info: maker_reserved.base_protocol_info,
rel_protocol_info: maker_reserved.rel_protocol_info,
swap_version: maker_reserved.swap_version,
premium: maker_reserved.premium.map(|p| p.to_ratio()),
})
}
}
Expand Down Expand Up @@ -2998,6 +3068,7 @@ struct StateMachineParams<'a> {
locktime: &'a u64,
maker_amount: &'a MmNumber,
taker_amount: &'a MmNumber,
taker_premium: &'a MmNumber,
}

#[cfg_attr(test, mockable)]
Expand Down Expand Up @@ -3110,6 +3181,7 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO
locktime: &lock_time,
maker_amount: &maker_amount,
taker_amount: &taker_amount,
taker_premium: &maker_order.premium.clone().unwrap_or_default(),
};
let taker_p2p_pubkey = match taker_p2p_pubkey {
PublicKey::Secp256k1(pubkey) => pubkey.into(),
Expand Down Expand Up @@ -3212,7 +3284,7 @@ async fn start_maker_swap_state_machine<
secret: *secret,
taker_coin: taker_coin.clone(),
taker_volume: params.taker_amount.clone(),
taker_premium: Default::default(),
taker_premium: params.taker_premium.clone(),
conf_settings: *params.my_conf_settings,
p2p_topic: swap_v2_topic(params.uuid),
uuid: *params.uuid,
Expand Down Expand Up @@ -3342,6 +3414,7 @@ fn lp_connected_alice(ctx: MmArc, taker_order: TakerOrder, taker_match: TakerMat
locktime: &locktime,
maker_amount: &maker_amount,
taker_amount: &taker_amount,
taker_premium: &taker_match.reserved.premium.unwrap_or_default(),
};
let maker_p2p_pubkey = match maker_p2p_pubkey {
PublicKey::Secp256k1(pubkey) => pubkey.into(),
Expand Down Expand Up @@ -3451,7 +3524,7 @@ async fn start_taker_swap_state_machine<
maker_volume: params.maker_amount.clone(),
taker_coin: taker_coin.clone(),
taker_volume: params.taker_amount.clone(),
taker_premium: Default::default(),
taker_premium: params.taker_premium.clone(),
secret_hash_algo: *params.secret_hash_algo,
conf_settings: *params.my_conf_settings,
p2p_topic: swap_v2_topic(params.uuid),
Expand Down Expand Up @@ -3983,6 +4056,7 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request:
base_protocol_info: Some(base_coin.coin_protocol_info(None)),
rel_protocol_info: Some(rel_coin.coin_protocol_info(Some(rel_amount.clone()))),
swap_version: order.swap_version,
premium: order.premium.clone(),
};
let topic = order.orderbook_topic();
log::debug!("Request matched sending reserved {:?}", reserved);
Expand Down Expand Up @@ -4711,6 +4785,8 @@ pub struct SetPriceReq {
rel_nota: Option<bool>,
#[serde(default = "get_true")]
save_in_history: bool,
#[serde(default)]
premium: Option<MmNumber>,
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -4978,7 +5054,8 @@ pub async fn create_maker_order(ctx: &MmArc, req: SetPriceReq) -> Result<MakerOr
.with_conf_settings(conf_settings)
.with_save_in_history(req.save_in_history)
.with_base_orderbook_ticker(ordermatch_ctx.orderbook_ticker(base_coin.ticker()))
.with_rel_orderbook_ticker(ordermatch_ctx.orderbook_ticker(rel_coin.ticker()));
.with_rel_orderbook_ticker(ordermatch_ctx.orderbook_ticker(rel_coin.ticker()))
.with_premium(req.premium);
if !ctx.use_trading_proto_v2() {
builder.set_legacy_swap_v();
}
Expand Down Expand Up @@ -5195,7 +5272,7 @@ pub async fn update_maker_order_rpc(ctx: MmArc, req: Json) -> Result<Response<Ve
/// Result of match_order_and_request function
#[derive(Debug, PartialEq)]
enum OrderMatchResult {
/// Order and request matched, contains base and rel resulting amounts
/// Order and request matched, contains base and rel resulting amounts (represent maker and taker payment amounts for swap)
Matched((MmNumber, MmNumber)),
/// Orders didn't match
NotMatched,
Expand Down
2 changes: 2 additions & 0 deletions mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ mod tests {
use itertools::Itertools;
use mm2_core::mm_ctx::MmCtxBuilder;
use mm2_db::indexed_db::TableSignature;
use mm2_number::MmNumber;
use mm2_rpc::data::legacy::{MatchBy, OrderType, TakerAction};
use std::collections::HashMap;
use wasm_bindgen_test::*;
Expand All @@ -726,6 +727,7 @@ mod tests {
rel_orderbook_ticker: None,
p2p_privkey: None,
swap_version: SwapVersion::default(),
premium: Default::default(),
}
}

Expand Down
3 changes: 3 additions & 0 deletions mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@ pub struct MakerReserved {
pub rel_protocol_info: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "SwapVersion::is_legacy")]
pub swap_version: SwapVersion,
/// Note: `std::default::Default` is not implemented for `num_rational::Ratio<mm2_number::BigInt>`, that's why it's preferable to use Optional
#[serde(default, skip_serializing_if = "Option::is_none")]
pub premium: Option<BigRational>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
Expand Down
1 change: 1 addition & 0 deletions mm2src/mm2_main/src/lp_ordermatch/simple_market_maker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ async fn create_single_order(
rel_confs: cfg.rel_confs,
rel_nota: cfg.rel_nota,
save_in_history: true,
premium: None,
};

let resp = create_maker_order(&ctx, req)
Expand Down
10 changes: 6 additions & 4 deletions mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,8 @@ impl<MakerCoin: MmCoin + MakerCoinSwapOpsV2, TakerCoin: MmCoin + TakerCoinSwapOp
swap_uuid: self.uuid,
locked_amount: LockedAmount {
coin: taker_coin_ticker.clone(),
amount: &(&self.taker_volume + &self.dex_fee().total_spend_amount()) + &self.taker_premium,
// The premium was taken into account during order matching, there is no need to add it here
amount: &self.taker_volume + &self.dex_fee().total_spend_amount(),
trade_fee: Some(taker_payment_fee.clone().into()),
},
};
Expand Down Expand Up @@ -921,7 +922,8 @@ impl<MakerCoin: MmCoin + MakerCoinSwapOpsV2, TakerCoin: MmCoin + TakerCoinSwapOp
swap_uuid: self.uuid,
locked_amount: LockedAmount {
coin: taker_coin_ticker.clone(),
amount: &(&self.taker_volume + &self.dex_fee().total_spend_amount()) + &self.taker_premium,
// The premium was taken into account during order matching, there is no need to add it here
amount: &self.taker_volume + &self.dex_fee().total_spend_amount(),
trade_fee: Some(taker_payment_fee.into()),
},
};
Expand Down Expand Up @@ -994,8 +996,8 @@ impl<MakerCoin: MmCoin + MakerCoinSwapOpsV2, TakerCoin: MmCoin + TakerCoinSwapOp
},
};

let total_payment_value = &(&state_machine.taker_volume + &state_machine.dex_fee().total_spend_amount())
+ &state_machine.taker_premium;
// The premium was taken into account during order matching, there is no need to add it here
let total_payment_value = &state_machine.taker_volume + &state_machine.dex_fee().total_spend_amount();
let preimage_value = TradePreimageValue::Exact(total_payment_value.to_decimal());
let stage = FeeApproxStage::StartSwap;

Expand Down
Loading
Loading