Skip to content

Commit 098f938

Browse files
committed
Custom order simulation
1 parent 388a9e5 commit 098f938

5 files changed

Lines changed: 278 additions & 110 deletions

File tree

crates/e2e/tests/e2e/order_simulation.rs

Lines changed: 65 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use {
1010
number::units::EthUnit,
1111
orderbook::dto::OrderSimulationResult,
1212
reqwest::StatusCode,
13+
serde_json::json,
1314
simulator::tenderly::dto::SimulationType,
1415
};
1516

@@ -19,33 +20,27 @@ async fn local_node_order_simulation() {
1920
run_test(order_simulation).await;
2021
}
2122

22-
async fn order_simulation(web3: Web3) {
23+
#[tokio::test]
24+
#[ignore]
25+
async fn local_node_custom_order_simulation() {
26+
run_test(custom_order_simulation).await;
27+
}
28+
29+
#[tokio::test]
30+
#[ignore]
31+
async fn local_node_order_simulation_block_number() {
32+
run_test(order_simulation_block_number).await;
33+
}
34+
35+
async fn custom_order_simulation(web3: Web3) {
2336
let mut onchain = OnchainComponents::deploy(web3.clone()).await;
2437

2538
let [solver] = onchain.make_solvers(10u64.eth()).await;
26-
let [trader] = onchain.make_accounts(10u64.eth()).await;
39+
let [trader] = onchain.make_accounts(1u64.eth()).await;
2740
let [token] = onchain
2841
.deploy_tokens_with_weth_uni_v2_pools(1_000u64.eth(), 1_000u64.eth())
2942
.await;
3043

31-
onchain
32-
.contracts()
33-
.weth
34-
.deposit()
35-
.from(trader.address())
36-
.value(3u64.eth())
37-
.send_and_watch()
38-
.await
39-
.unwrap();
40-
onchain
41-
.contracts()
42-
.weth
43-
.approve(onchain.contracts().allowance, 3u64.eth())
44-
.from(trader.address())
45-
.send_and_watch()
46-
.await
47-
.unwrap();
48-
4944
let services = Services::new(&onchain).await;
5045
services
5146
.start_protocol_with_args(
@@ -55,47 +50,57 @@ async fn order_simulation(web3: Web3) {
5550
)
5651
.await;
5752

58-
let order = OrderCreation {
59-
sell_token: *onchain.contracts().weth.address(),
60-
sell_amount: 2u64.eth(),
61-
buy_token: *token.address(),
62-
buy_amount: 1u64.eth(),
63-
valid_to: model::time::now_in_epoch_seconds() + 300,
64-
kind: OrderKind::Buy,
65-
..Default::default()
66-
}
67-
.sign(
68-
EcdsaSigningScheme::Eip712,
69-
&onchain.contracts().domain_separator,
70-
&trader.signer,
71-
);
72-
let uid = services.create_order(&order).await.unwrap();
73-
7453
let client = services.client();
54+
let sell_amount = 1u64.eth();
55+
56+
let body = json!({
57+
"sellToken": token.address(),
58+
"buyToken": onchain.contracts().weth.address(),
59+
"sellAmount": sell_amount.to_string(),
60+
"buyAmount": "1",
61+
"kind": "sell",
62+
"owner": trader.address(),
63+
});
64+
65+
// Trader has no sell tokens — simulation should revert.
7566
let response = client
76-
.get(format!("{API_HOST}/api/v1/debug/simulation/{uid}"))
67+
.post(format!("{API_HOST}/api/v1/debug/simulation"))
68+
.json(&body)
7769
.send()
7870
.await
7971
.unwrap();
8072
assert_eq!(response.status(), StatusCode::OK);
81-
let response = response.json::<OrderSimulationResult>().await.unwrap();
82-
assert_eq!(response.error, None);
73+
let result = response.json::<OrderSimulationResult>().await.unwrap();
74+
assert!(
75+
result.error.is_some(),
76+
"expected simulation error when trader has no funds"
77+
);
8378

84-
let tenderly = response.tenderly_request;
85-
// check if the fields that are directly derived from the simulation have
86-
// correct values in the tenderly request object
87-
assert_eq!(tenderly.to, *onchain.contracts().gp_settlement.address());
88-
assert_eq!(tenderly.simulation_type, Some(SimulationType::Full));
89-
assert_eq!(tenderly.value, None);
90-
}
79+
// Fund the trader and approve the vault relayer.
80+
token.mint(trader.address(), sell_amount).await;
81+
token
82+
.approve(onchain.contracts().allowance, sell_amount)
83+
.from(trader.address())
84+
.send_and_watch()
85+
.await
86+
.unwrap();
9187

92-
#[tokio::test]
93-
#[ignore]
94-
async fn local_node_order_simulation_block_number() {
95-
run_test(order_simulation_block_number).await;
88+
// Simulation should now succeed.
89+
let response = client
90+
.post(format!("{API_HOST}/api/v1/debug/simulation"))
91+
.json(&body)
92+
.send()
93+
.await
94+
.unwrap();
95+
assert_eq!(response.status(), StatusCode::OK);
96+
let result = response.json::<OrderSimulationResult>().await.unwrap();
97+
assert!(
98+
result.error.is_none(),
99+
"expected simulation to pass after funding the trader"
100+
);
96101
}
97102

98-
async fn order_simulation_block_number(web3: Web3) {
103+
async fn order_simulation(web3: Web3) {
99104
let mut onchain = OnchainComponents::deploy(web3.clone()).await;
100105

101106
let [solver] = onchain.make_solvers(10u64.eth()).await;
@@ -104,7 +109,6 @@ async fn order_simulation_block_number(web3: Web3) {
104109
.deploy_tokens_with_weth_uni_v2_pools(1_000u64.eth(), 1_000u64.eth())
105110
.await;
106111

107-
// Fund trader so the order passes balance validation at submission time.
108112
onchain
109113
.contracts()
110114
.weth
@@ -148,69 +152,22 @@ async fn order_simulation_block_number(web3: Web3) {
148152
);
149153
let uid = services.create_order(&order).await.unwrap();
150154

151-
// Transfer all WETH away from the trader — now they have no sell-token
152-
// balance. The current block becomes the "no funds" snapshot.
153-
let burn = Address::from([0x42u8; 20]);
154-
onchain
155-
.contracts()
156-
.weth
157-
.transfer(burn, 3u64.eth())
158-
.from(trader.address())
159-
.send_and_watch()
160-
.await
161-
.unwrap();
162-
let block_no_funds = web3.provider.get_block_number().await.unwrap();
163-
164-
// Re-deposit WETH. The current block now has the trader fully funded again.
165-
onchain
166-
.contracts()
167-
.weth
168-
.deposit()
169-
.from(trader.address())
170-
.value(3u64.eth())
171-
.send_and_watch()
172-
.await
173-
.unwrap();
174-
let block_with_funds = web3.provider.get_block_number().await.unwrap();
175-
176155
let client = services.client();
177-
178-
// Simulation at the block where the trader had no WETH must fail.
179156
let response = client
180-
.get(format!(
181-
"{API_HOST}/api/v1/debug/simulation/{uid}?block_number={block_no_funds}"
182-
))
183-
.send()
184-
.await
185-
.unwrap();
186-
assert_eq!(response.status(), StatusCode::OK);
187-
let result = response.json::<OrderSimulationResult>().await.unwrap();
188-
assert!(
189-
result.error.is_some(),
190-
"expected simulation failure at block {block_no_funds} (no funds), got success"
191-
);
192-
193-
// Simulation at the block where the trader has WETH must succeed.
194-
let response = client
195-
.get(format!(
196-
"{API_HOST}/api/v1/debug/simulation/{uid}?block_number={block_with_funds}"
197-
))
157+
.get(format!("{API_HOST}/api/v1/debug/simulation/{uid}"))
198158
.send()
199159
.await
200160
.unwrap();
201161
assert_eq!(response.status(), StatusCode::OK);
202-
let result = response.json::<OrderSimulationResult>().await.unwrap();
203-
assert_eq!(
204-
result.error, None,
205-
"expected simulation success at block {block_with_funds} (funded), got error: {:?}",
206-
result.error
207-
);
208-
}
162+
let response = response.json::<OrderSimulationResult>().await.unwrap();
163+
assert_eq!(response.error, None);
209164

210-
#[tokio::test]
211-
#[ignore]
212-
async fn local_node_order_simulation_block_number() {
213-
run_test(order_simulation_block_number).await;
165+
let tenderly = response.tenderly_request;
166+
// check if the fields that are directly derived from the simulation have
167+
// correct values in the tenderly request object
168+
assert_eq!(tenderly.to, *onchain.contracts().gp_settlement.address());
169+
assert_eq!(tenderly.simulation_type, Some(SimulationType::Full));
170+
assert_eq!(tenderly.value, None);
214171
}
215172

216173
async fn order_simulation_block_number(web3: Web3) {

crates/orderbook/openapi.yml

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,31 @@ paths:
765765
$ref: "#/components/schemas/TotalSurplus"
766766
"400":
767767
description: Invalid address.
768+
"/api/v1/debug/simulation":
769+
post:
770+
operationId: debugSimulationPost
771+
summary: Simulate an arbitrary order.
772+
description: >
773+
Simulates an arbitrary order specified in the request body and returns
774+
the Tenderly simulation request, along with any simulation error if
775+
applicable.
776+
requestBody:
777+
required: true
778+
content:
779+
application/json:
780+
schema:
781+
$ref: "#/components/schemas/SimulationRequest"
782+
responses:
783+
"200":
784+
description: Simulation request returned.
785+
content:
786+
application/json:
787+
schema:
788+
$ref: "#/components/schemas/OrderSimulation"
789+
"405":
790+
description: Order simulation endpoint is not enabled.
791+
"500":
792+
description: Internal error.
768793
"/api/v1/debug/simulation/{uid}":
769794
get:
770795
operationId: debugSimulation
@@ -2650,6 +2675,79 @@ components:
26502675
- from
26512676
- to
26522677
- input
2678+
SimulationRequest:
2679+
description: >
2680+
Request body for simulating an arbitrary order without it being stored
2681+
in the orderbook.
2682+
type: object
2683+
properties:
2684+
sellToken:
2685+
description: The token being sold.
2686+
allOf:
2687+
- $ref: "#/components/schemas/Address"
2688+
buyToken:
2689+
description: The token being bought.
2690+
allOf:
2691+
- $ref: "#/components/schemas/Address"
2692+
sellAmount:
2693+
description: Amount of sell token (hex- or decimal-encoded uint256).
2694+
allOf:
2695+
- $ref: "#/components/schemas/TokenAmount"
2696+
buyAmount:
2697+
description: Amount of buy token (hex- or decimal-encoded uint256).
2698+
allOf:
2699+
- $ref: "#/components/schemas/TokenAmount"
2700+
kind:
2701+
description: Whether this is a sell or buy order.
2702+
allOf:
2703+
- $ref: "#/components/schemas/OrderKind"
2704+
owner:
2705+
description: The address of the order owner.
2706+
allOf:
2707+
- $ref: "#/components/schemas/Address"
2708+
receiver:
2709+
description: >
2710+
The address that will receive the buy tokens. Defaults to the owner
2711+
if omitted.
2712+
allOf:
2713+
- $ref: "#/components/schemas/Address"
2714+
nullable: true
2715+
sellTokenBalance:
2716+
description: Where the sell token should be drawn from.
2717+
allOf:
2718+
- $ref: "#/components/schemas/SellTokenSource"
2719+
default: erc20
2720+
buyTokenBalance:
2721+
description: Where the buy token should be transferred to.
2722+
allOf:
2723+
- $ref: "#/components/schemas/BuyTokenDestination"
2724+
default: erc20
2725+
appData:
2726+
description: >
2727+
Full app data JSON string. Defaults to `"{}"` if omitted.
2728+
type: string
2729+
nullable: true
2730+
interactions:
2731+
description: Pre- and post-trade interactions for the order.
2732+
type: object
2733+
properties:
2734+
pre:
2735+
description: Interactions executed before the order's trade.
2736+
type: array
2737+
items:
2738+
$ref: "#/components/schemas/InteractionData"
2739+
post:
2740+
description: Interactions executed after the order's trade.
2741+
type: array
2742+
items:
2743+
$ref: "#/components/schemas/InteractionData"
2744+
required:
2745+
- sellToken
2746+
- buyToken
2747+
- sellAmount
2748+
- buyAmount
2749+
- kind
2750+
- owner
26532751
OrderSimulation:
26542752
description: >
26552753
The Tenderly simulation request for an order, along with any

crates/orderbook/src/api.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,11 @@ pub fn handle_all_routes(
271271
"/api/v1/debug/simulation/{uid}",
272272
get(debug_simulation::debug_simulation_handler),
273273
),
274+
(
275+
"POST",
276+
"/api/v1/debug/simulation",
277+
post(debug_simulation::debug_simulation_post_handler),
278+
),
274279
// V2 routes
275280
// /solver_competition routes (specific before parameterized)
276281
(

0 commit comments

Comments
 (0)