Skip to content

Commit 227bfb5

Browse files
committed
feat(rfq): quote leftover capacity instead of rejecting size
- Change capacity check to scale down requests instead of rejecting with Size reason - When input exceeds available capacity, quote the remaining available amount rather than inventory-reject - Update test expectations: requests that previously rejected now receive quotes for leftover slices - Allows the API to bundle multiple makers into a single taker quote when capacity is constrained - Recalculate output, fee, and sell_amount for scaled-down input amounts - Preserve inventory rejection only when zero capacity remains after reservations
1 parent 282874f commit 227bfb5

6 files changed

Lines changed: 60 additions & 28 deletions

File tree

.textile-monorepo-source

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
86206091c13f44d3c3c78c9f3215782a1ed08253
1+
1d81a9f9f390d7c73df7ad5d4cdfcb467e9ca7d4

.textile-stitch-release-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.195
1+
0.1.196

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "stitch-bot"
3-
version = "0.1.195"
3+
version = "0.1.196"
44
edition = "2021"
55
description = "Stitch — Textile filler-network operator bot; market-makes the filler order book with signed UniswapX limit orders."
66
license = "AGPL-3.0-or-later"

src/rfq/mod.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -858,14 +858,15 @@ mod tests {
858858
);
859859
let _ = sig;
860860

861-
// The quote's input is reserved: a second identical request would
862-
// need 2 × 979902009 > the 1.5e9 capacity → inventory reject.
861+
// The quote's input is reserved: a second identical request used to
862+
// inventory-reject (2 × 979902009 > 1.5e9). It now quotes the leftover
863+
// so the venue can bundle this slice with other makers.
863864
let reply = engine.respond(exact_input_request("rfq_2"), &prices).await;
864-
let MakerFrame::QuoteReject(rej) = reply else {
865-
panic!("expected an inventory reject, got {reply:?}");
865+
let MakerFrame::QuoteResponse(resp) = reply else {
866+
panic!("expected a leftover quote, got {reply:?}");
866867
};
867-
assert_eq!(rej.reason, RejectReason::Inventory);
868-
assert_eq!(engine.reservations.len(), 1);
868+
assert_eq!(resp.buy_amount, "520097991");
869+
assert_eq!(engine.reservations.len(), 2);
869870
}
870871

871872
#[tokio::test]

src/rfq/responder.rs

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -234,13 +234,25 @@ pub fn decide_quote(
234234
let Some(capacity) = capacity else {
235235
return Err(RejectReason::Inventory);
236236
};
237-
if input > capacity {
238-
return Err(RejectReason::Size);
239-
}
240237
let reserved = if bid { reserved_bid } else { reserved_ask };
241-
if input > capacity.saturating_sub(reserved) {
238+
let available = capacity.saturating_sub(reserved);
239+
if available.is_zero() {
242240
return Err(RejectReason::Inventory);
243241
}
242+
// The venue may ask for more than we can fill (stale levels, or a slice
243+
// of a larger RFQ). Quote what remains instead of rejecting — the API
244+
// bundles several makers into one taker quote.
245+
let (input, output, fee, sell_amount) = if input > available {
246+
let input = available;
247+
let output = output_for_input(input);
248+
let fee = fee_on(output, req.fee_bps);
249+
if input.is_zero() || output.is_zero() || fee.is_zero() {
250+
return Err(RejectReason::Size);
251+
}
252+
(input, output, fee, output + fee)
253+
} else {
254+
(input, output, fee, sell_amount)
255+
};
244256

245257
Ok(QuotePlan {
246258
bid,
@@ -436,23 +448,28 @@ mod tests {
436448
}
437449

438450
#[test]
439-
fn capacity_and_reservations_gate_with_distinct_reasons() {
440-
// Bigger than configured capacity outright → size.
451+
fn capacity_scales_down_instead_of_rejecting_size() {
452+
// Bigger than configured capacity: quote the cap, don't reject.
441453
let mut req = request(COLLATERAL, DEBT);
442454
req.sell_amount = Some("100000000000".into()); // maker would pay ~98e9 > 5e9
443-
assert_eq!(
444-
decide(&book(), &req, 1.0, U256::ZERO, U256::ZERO),
445-
Err(RejectReason::Size)
446-
);
455+
let plan = decide(&book(), &req, 1.0, U256::ZERO, U256::ZERO).unwrap();
456+
assert_eq!(plan.input, U256::from(5_000_000_000u64));
457+
assert!(plan.sell_amount < U256::from(100000000000u64));
447458

448-
// Fits capacity but not what's left after in-flight quotes → inventory.
459+
// Fits capacity but nothing left after in-flight quotes → inventory.
449460
let mut req = request(COLLATERAL, DEBT);
450-
req.sell_amount = Some("1000000000".into()); // maker pays ~0.98e9
451-
let reserved_bid = U256::from(4_500_000_000u64); // 4.5e9 of 5e9 claimed
461+
req.sell_amount = Some("1000000000".into());
462+
let fully_reserved = U256::from(5_000_000_000u64);
452463
assert_eq!(
453-
decide(&book(), &req, 1.0, reserved_bid, U256::ZERO),
464+
decide(&book(), &req, 1.0, fully_reserved, U256::ZERO),
454465
Err(RejectReason::Inventory)
455466
);
467+
468+
// Partial reservation: quote what's left rather than inventory-reject.
469+
let reserved_bid = U256::from(4_500_000_000u64); // 0.5e9 left
470+
let plan = decide(&book(), &req, 1.0, reserved_bid, U256::ZERO).unwrap();
471+
assert_eq!(plan.input, U256::from(500_000_000u64));
472+
456473
// The ask side's reservations don't bleed into the bid check.
457474
assert!(decide(&book(), &req, 1.0, U256::ZERO, reserved_bid).is_ok());
458475
}
@@ -641,13 +658,25 @@ mod tests {
641658
]));
642659
assert!(decide_quote(&wallet_book, &req, 1.0, U256::ZERO, U256::ZERO, &inv).is_ok());
643660

644-
// Reservations still eat the live balance.
661+
// Reservations eat the live balance; leftover is still quoted.
662+
let plan = decide_quote(
663+
&wallet_book,
664+
&req,
665+
1.0,
666+
U256::from(1_500_000_000u64),
667+
U256::ZERO,
668+
&inv,
669+
)
670+
.unwrap();
671+
assert_eq!(plan.input, U256::from(500_000_000u64));
672+
673+
// Nothing left → inventory.
645674
assert_eq!(
646675
decide_quote(
647676
&wallet_book,
648677
&req,
649678
1.0,
650-
U256::from(1_500_000_000u64),
679+
U256::from(2_000_000_000u64),
651680
U256::ZERO,
652681
&inv
653682
),
@@ -676,9 +705,11 @@ mod tests {
676705
(debt, U256::from(100_000u64)),
677706
(collateral, U256::from(100_000u64)),
678707
]));
708+
let thin_plan = decide_quote(&exact, &req, 1.0, U256::ZERO, U256::ZERO, &thin).unwrap();
679709
assert_eq!(
680-
decide_quote(&exact, &req, 1.0, U256::ZERO, U256::ZERO, &thin),
681-
Err(RejectReason::Size)
710+
thin_plan.input,
711+
U256::from(100_000u64),
712+
"thin wallet is quoted as a leftover slice, not size-rejected"
682713
);
683714
assert!(decide_quote(&exact, &req, 1.0, U256::ZERO, U256::ZERO, &inv).is_ok());
684715
}

0 commit comments

Comments
 (0)