Skip to content

Commit 938ada0

Browse files
committed
feat: honor preferred filler order windows
- Add `additionalValidationContract` and `additionalValidationData` fields to `OrderParams` struct to support UniswapX validation callbacks - Update EIP-712 order info hashing to use actual validation contract and data instead of hardcoded zero address and empty bytes - Extend `RestingOrder` to include additional validation fields parsed from indexer responses - Update resting limit orders GraphQL query to request validation fields and filter by filler wallet - Pass filler wallet address to indexer when querying resting limit orders - Update order encoding to properly serialize variable-length validation data within OrderInfo structure - Initialize validation fields to zero/empty in all order creation paths (poster, submit tests, taker tests)
1 parent 256349c commit 938ada0

10 files changed

Lines changed: 57 additions & 17 deletions

.textile-monorepo-source

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5e2592c55a6c3cae76a904c4c544fc7893a455cd
1+
b2f439ffb798cf657f9f1dee4d033a76750c1414

.textile-stitch-release-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.122
1+
0.1.123

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.122"
3+
version = "0.1.123"
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/eip712.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,14 @@ fn hash_words(words: &[[u8; 32]]) -> B256 {
8181
}
8282

8383
fn order_info_hash(o: &OrderParams) -> B256 {
84-
let empty_validation_data = keccak256([]); // keccak256("")
8584
hash_words(&[
8685
b256_word(k(ORDER_INFO_TYPE)),
8786
addr_word(o.reactor),
8887
addr_word(o.swapper),
8988
u256_word(o.nonce),
9089
u256_word(o.deadline),
91-
addr_word(Address::ZERO), // additionalValidationContract
92-
b256_word(empty_validation_data),
90+
addr_word(o.additional_validation_contract),
91+
b256_word(keccak256(&o.additional_validation_data)),
9392
])
9493
}
9594

@@ -175,6 +174,8 @@ mod tests {
175174
output_token: address!("4444444444444444444444444444444444444444"), // cNGN
176175
output_amount: U256::from(1_550_000_000u64),
177176
recipient: address!("2222222222222222222222222222222222222222"),
177+
additional_validation_contract: Address::ZERO,
178+
additional_validation_data: Default::default(),
178179
}
179180
}
180181

src/indexer.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ const COMMITTED_INPUT_QUERY: &str =
1616
"query CommittedInput($chainId: Int!, $maker: String!, $inputToken: String!) { \
1717
fillerCommittedInput(chainId: $chainId, maker: $maker, inputToken: $inputToken) }";
1818
const RESTING_LIMIT_ORDERS_QUERY: &str =
19-
"query RestingLimitOrders($chainId: Int!, $inputToken: String!, $outputToken: String!) { \
20-
restingLimitOrders(chainId: $chainId, inputToken: $inputToken, outputToken: $outputToken) { \
21-
id reactor maker inputToken inputAmount outputToken outputAmount rateRay nonce deadlineSec signature } }";
19+
"query RestingLimitOrders($chainId: Int!, $inputToken: String!, $outputToken: String!, $fillerWallet: String!) { \
20+
restingLimitOrders(chainId: $chainId, inputToken: $inputToken, outputToken: $outputToken, fillerWallet: $fillerWallet) { \
21+
id reactor maker inputToken inputAmount outputToken outputAmount rateRay nonce deadlineSec \
22+
additionalValidationContract additionalValidationData signature } }";
2223

2324
/// Build the GraphQL request body for one order (pure — easy to assert on).
2425
pub fn build_submit_request(order: &SubmitOrder) -> Value {
@@ -53,13 +54,15 @@ pub fn build_resting_limit_orders_request(
5354
chain_id: u64,
5455
input_token: &str,
5556
output_token: &str,
57+
filler_wallet: &str,
5658
) -> Value {
5759
json!({
5860
"query": RESTING_LIMIT_ORDERS_QUERY,
5961
"variables": {
6062
"chainId": chain_id,
6163
"inputToken": input_token,
6264
"outputToken": output_token,
65+
"fillerWallet": filler_wallet,
6366
}
6467
})
6568
}
@@ -163,8 +166,10 @@ impl Indexer {
163166
chain_id: u64,
164167
input_token: &str,
165168
output_token: &str,
169+
filler_wallet: &str,
166170
) -> anyhow::Result<Value> {
167-
let body = build_resting_limit_orders_request(chain_id, input_token, output_token);
171+
let body =
172+
build_resting_limit_orders_request(chain_id, input_token, output_token, filler_wallet);
168173
let resp = self.post_graphql(&body, "resting limit orders").await?;
169174
Ok(resp
170175
.pointer("/data/restingLimitOrders")

src/poster.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ impl Poster<'_> {
8383
output_token,
8484
output_amount: draft.output_amount,
8585
recipient: self.maker,
86+
additional_validation_contract: Address::ZERO,
87+
additional_validation_data: Default::default(),
8688
};
8789
Some((order, draft.client_order_id.clone()))
8890
})
@@ -274,6 +276,8 @@ mod tests {
274276
output_token: Address::ZERO,
275277
output_amount: U256::from(1u64),
276278
recipient: Address::ZERO,
279+
additional_validation_contract: Address::ZERO,
280+
additional_validation_data: Default::default(),
277281
}
278282
}
279283

src/submit.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ mod tests {
7979
output_token: address!("4444444444444444444444444444444444444444"),
8080
output_amount: U256::from(1_550_000_000u64),
8181
recipient: maker,
82+
additional_validation_contract: Address::ZERO,
83+
additional_validation_data: Default::default(),
8284
};
8385

8486
let s = sign_submission(&order, PERMIT2, 8453, &signer)

src/taker.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ pub struct RestingOrder {
6666
pub output_amount: U256,
6767
pub nonce: U256,
6868
pub deadline_sec: u64,
69+
pub additional_validation_contract: Address,
70+
pub additional_validation_data: Bytes,
6971
/// 65-byte EIP-712 signature over the Permit2 witness digest.
7072
pub signature: Vec<u8>,
7173
}
@@ -85,6 +87,8 @@ impl RestingOrder {
8587
output_token: self.output_token,
8688
output_amount: self.output_amount,
8789
recipient: self.maker,
90+
additional_validation_contract: self.additional_validation_contract,
91+
additional_validation_data: self.additional_validation_data.clone(),
8892
}
8993
}
9094
}
@@ -109,6 +113,11 @@ fn parse_signature_field(v: &Value) -> Option<Vec<u8>> {
109113
(bytes.len() == 65).then_some(bytes)
110114
}
111115

116+
fn parse_bytes_field(v: &Value) -> Option<Bytes> {
117+
let value = v.as_str()?;
118+
alloy_primitives::hex::decode(value).ok().map(Bytes::from)
119+
}
120+
112121
/// Parse the `restingLimitOrders` response rows; malformed rows are dropped
113122
/// (they can only have come from a broken or hostile indexer).
114123
pub fn parse_resting_orders(rows: &Value) -> Vec<RestingOrder> {
@@ -126,6 +135,14 @@ pub fn parse_resting_orders(rows: &Value) -> Vec<RestingOrder> {
126135
output_amount: parse_u256_field(row.get("outputAmount")?)?,
127136
nonce: parse_u256_field(row.get("nonce")?)?,
128137
deadline_sec: parse_u256_field(row.get("deadlineSec")?)?.try_into().ok()?,
138+
additional_validation_contract: row
139+
.get("additionalValidationContract")
140+
.and_then(parse_address_field)
141+
.unwrap_or(Address::ZERO),
142+
additional_validation_data: row
143+
.get("additionalValidationData")
144+
.and_then(parse_bytes_field)
145+
.unwrap_or_default(),
129146
signature: parse_signature_field(row.get("signature")?)?,
130147
})
131148
})
@@ -294,7 +311,9 @@ fn word_address(a: Address) -> [u8; 32] {
294311
/// encoding locally from verified fields is what keeps a hostile indexer's
295312
/// `encodedOrder` out of the transaction entirely.
296313
pub fn encode_order_bytes(o: &OrderParams) -> Vec<u8> {
297-
let mut out = Vec::with_capacity(17 * 32);
314+
let validation_data = padded_bytes(&o.additional_validation_data);
315+
let info_size = 6 * 32 + validation_data.len();
316+
let mut out = Vec::with_capacity(10 * 32 + info_size);
298317
out.extend_from_slice(&word_usize(0x20)); // offset to the tuple
299318

300319
// Tuple head: [info offset, input.token, input.amount, input.maxAmount,
@@ -304,16 +323,16 @@ pub fn encode_order_bytes(o: &OrderParams) -> Vec<u8> {
304323
out.extend_from_slice(&word_address(o.input_token));
305324
out.extend_from_slice(&word_u256(o.input_amount));
306325
out.extend_from_slice(&word_u256(o.input_amount)); // maxAmount == amount
307-
out.extend_from_slice(&word_usize(5 * 32 + 7 * 32)); // outputs after info
326+
out.extend_from_slice(&word_usize(5 * 32 + info_size)); // outputs after info
308327

309-
// OrderInfo: 5 static words + offset to empty `additionalValidationData`.
328+
// OrderInfo: 5 static words + offset to `additionalValidationData`.
310329
out.extend_from_slice(&word_address(o.reactor));
311330
out.extend_from_slice(&word_address(o.swapper));
312331
out.extend_from_slice(&word_u256(o.nonce));
313332
out.extend_from_slice(&word_u256(o.deadline));
314-
out.extend_from_slice(&word_address(Address::ZERO));
333+
out.extend_from_slice(&word_address(o.additional_validation_contract));
315334
out.extend_from_slice(&word_usize(6 * 32)); // bytes offset within info
316-
out.extend_from_slice(&word_usize(0)); // len(additionalValidationData)
335+
out.extend_from_slice(&validation_data);
317336

318337
// OutputToken[1]
319338
out.extend_from_slice(&word_usize(1));
@@ -476,6 +495,7 @@ async fn take_direction_once(
476495
chain_id,
477496
&input_token.to_string(),
478497
&output_token.to_string(),
498+
&wallet.address().to_string(),
479499
)
480500
.await?;
481501
let orders = parse_resting_orders(&rows);
@@ -648,6 +668,8 @@ mod tests {
648668
output_amount: U256::from(1_000_000u64),
649669
nonce: U256::from(42u64),
650670
deadline_sec,
671+
additional_validation_contract: Address::ZERO,
672+
additional_validation_data: Default::default(),
651673
signature: vec![],
652674
};
653675
let digest = permit2_digest(&order.params(), PERMIT2, CHAIN);
@@ -834,6 +856,8 @@ mod tests {
834856
output_token: CNGN,
835857
output_amount: U256::from(1_550_000_000u64),
836858
recipient: address!("2222222222222222222222222222222222222222"),
859+
additional_validation_contract: Address::ZERO,
860+
additional_validation_data: Default::default(),
837861
}
838862
}
839863

src/types.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Copyright (c) 2026 Textile, Inc.
33
//! Shared types for the operator bot.
44
5-
pub use alloy_primitives::{Address, B256, U256};
5+
pub use alloy_primitives::{Address, Bytes, B256, U256};
66

77
/// One operator limit order: pay `input_amount` debt (USDT) to buy
88
/// `output_amount` collateral (cNGN). For a limit order (no Dutch decay) the
@@ -28,4 +28,8 @@ pub struct OrderParams {
2828
pub output_amount: U256,
2929
/// Where the bought collateral lands (the operator's wallet).
3030
pub recipient: Address,
31+
/// Optional UniswapX validation callback; zero for operator quote orders.
32+
pub additional_validation_contract: Address,
33+
/// ABI-encoded callback parameters; empty for operator quote orders.
34+
pub additional_validation_data: Bytes,
3135
}

0 commit comments

Comments
 (0)