From a956569fcd906ba691667919d42919caae01c399 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Tue, 6 Feb 2024 14:16:45 +0100 Subject: [PATCH 01/10] Rename modules --- auction-server/src/api.rs | 16 ++++++++-------- auction-server/src/api/{rest.rs => bid.rs} | 0 .../src/api/{marketplace.rs => liquidation.rs} | 4 ++-- auction-server/src/liquidation_adapter.rs | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) rename auction-server/src/api/{rest.rs => bid.rs} (100%) rename auction-server/src/api/{marketplace.rs => liquidation.rs} (99%) diff --git a/auction-server/src/api.rs b/auction-server/src/api.rs index 5e1202cc..92847a32 100644 --- a/auction-server/src/api.rs +++ b/auction-server/src/api.rs @@ -1,12 +1,12 @@ use { crate::{ api::{ - marketplace::{ + bid::Bid, + liquidation::{ LiquidationOpportunity, OpportunityBid, TokenQty, }, - rest::Bid, }, auction::run_submission_loop, config::{ @@ -82,8 +82,8 @@ async fn root() -> String { format!("PER Auction Server API {}", crate_version!()) } -pub(crate) mod marketplace; -mod rest; +mod bid; +pub(crate) mod liquidation; #[derive(ToResponse, ToSchema)] #[response(description = "An error occurred processing the request")] @@ -218,18 +218,18 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> { let app: Router<()> = Router::new() .merge(SwaggerUi::new("/docs").url("/docs/openapi.json", ApiDoc::openapi())) .route("/", get(root)) - .route("/bid", post(rest::bid)) + .route("/bid", post(bid::bid)) .route( "/liquidation/submit_opportunity", - post(marketplace::submit_opportunity), + post(liquidation::submit_opportunity), ) .route( "/liquidation/fetch_opportunities", - get(marketplace::fetch_opportunities), + get(liquidation::fetch_opportunities), ) .route( "/liquidation/bid_opportunity", - post(marketplace::bid_opportunity), + post(liquidation::bid_opportunity), ) .layer(CorsLayer::permissive()) .with_state(server_store); diff --git a/auction-server/src/api/rest.rs b/auction-server/src/api/bid.rs similarity index 100% rename from auction-server/src/api/rest.rs rename to auction-server/src/api/bid.rs diff --git a/auction-server/src/api/marketplace.rs b/auction-server/src/api/liquidation.rs similarity index 99% rename from auction-server/src/api/marketplace.rs rename to auction-server/src/api/liquidation.rs index 24a9031d..fee12d83 100644 --- a/auction-server/src/api/marketplace.rs +++ b/auction-server/src/api/liquidation.rs @@ -1,7 +1,7 @@ use { crate::{ api::{ - rest::handle_bid, + bid::handle_bid, RestError, }, config::ChainId, @@ -243,7 +243,7 @@ pub async fn bid_opportunity( .map_err(|e| RestError::BadParameters(e.to_string()))?; match handle_bid( store.clone(), - crate::api::rest::Bid { + crate::api::bid::Bid { permission_key: liquidation.permission_key.clone(), chain_id: liquidation.chain_id.clone(), contract: chain_store.config.adapter_contract, diff --git a/auction-server/src/liquidation_adapter.rs b/auction-server/src/liquidation_adapter.rs index 82155974..197b0bed 100644 --- a/auction-server/src/liquidation_adapter.rs +++ b/auction-server/src/liquidation_adapter.rs @@ -1,6 +1,6 @@ use { crate::{ - api::marketplace::OpportunityBid, + api::liquidation::OpportunityBid, state::VerifiedLiquidationOpportunity, }, anyhow::{ From 23a6f0a5d4978c1c69aa252a12c75e4aede31b43 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Tue, 6 Feb 2024 14:17:06 +0100 Subject: [PATCH 02/10] Add /v1/ prefix to all paths --- auction-server/src/api.rs | 8 ++++---- auction-server/src/api/bid.rs | 5 +++-- auction-server/src/api/liquidation.rs | 6 +++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/auction-server/src/api.rs b/auction-server/src/api.rs index 92847a32..cf07c2c0 100644 --- a/auction-server/src/api.rs +++ b/auction-server/src/api.rs @@ -218,17 +218,17 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> { let app: Router<()> = Router::new() .merge(SwaggerUi::new("/docs").url("/docs/openapi.json", ApiDoc::openapi())) .route("/", get(root)) - .route("/bid", post(bid::bid)) + .route("/v1/bid", post(bid::bid)) .route( - "/liquidation/submit_opportunity", + "/v1/liquidation/submit_opportunity", post(liquidation::submit_opportunity), ) .route( - "/liquidation/fetch_opportunities", + "/v1/liquidation/fetch_opportunities", get(liquidation::fetch_opportunities), ) .route( - "/liquidation/bid_opportunity", + "/v1/liquidation/bid_opportunity", post(liquidation::bid_opportunity), ) .layer(CorsLayer::permissive()) diff --git a/auction-server/src/api/bid.rs b/auction-server/src/api/bid.rs index 4f6bf6a9..726931fc 100644 --- a/auction-server/src/api/bid.rs +++ b/auction-server/src/api/bid.rs @@ -101,8 +101,9 @@ pub async fn handle_bid(store: Arc, bid: Bid) -> Result) -> Vec<(Address, U256)> { /// /// The opportunity will be verified by the server. If the opportunity is valid, it will be stored in the database /// and will be available for bidding. -#[utoipa::path(post, path = "/liquidation/submit_opportunity", request_body = LiquidationOpportunity, responses( +#[utoipa::path(post, path = "/v1/liquidation/submit_opportunity", request_body = LiquidationOpportunity, responses( (status = 200, description = "Opportunity was stored succesfuly with the returned uuid", body = String), (status = 400, response=RestError) ),)] @@ -140,7 +140,7 @@ pub async fn submit_opportunity( } /// Fetch all liquidation opportunities ready to be exectued. -#[utoipa::path(get, path = "/liquidation/fetch_opportunities", responses( +#[utoipa::path(get, path = "/v1/liquidation/fetch_opportunities", responses( (status = 200, description = "Array of liquidation opportunities ready for bidding", body = Vec), (status = 400, response=RestError) ),)] @@ -207,7 +207,7 @@ pub struct OpportunityBid { } /// Bid on liquidation opportunity -#[utoipa::path(post, path = "/liquidation/bid_opportunity", request_body=OpportunityBid, responses( +#[utoipa::path(post, path = "/v1/liquidation/bid_opportunity", request_body=OpportunityBid, responses( (status = 200, description = "Bid Result", body = String), (status = 400, response=RestError) ),)] From a07417d91ca1455cc25aeaadbccedb7c095d957f Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Tue, 6 Feb 2024 14:46:38 +0100 Subject: [PATCH 03/10] Json error responses --- auction-server/src/api.rs | 64 ++++++++++++++------------- auction-server/src/api/bid.rs | 8 +++- auction-server/src/api/liquidation.rs | 10 +++-- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/auction-server/src/api.rs b/auction-server/src/api.rs index cf07c2c0..87d22f9c 100644 --- a/auction-server/src/api.rs +++ b/auction-server/src/api.rs @@ -34,6 +34,7 @@ use { get, post, }, + Json, Router, }, clap::crate_version, @@ -50,6 +51,7 @@ use { types::Bytes, }, futures::future::join_all, + serde::Serialize, std::{ collections::HashMap, sync::{ @@ -85,19 +87,13 @@ async fn root() -> String { mod bid; pub(crate) mod liquidation; -#[derive(ToResponse, ToSchema)] -#[response(description = "An error occurred processing the request")] pub enum RestError { /// The request contained invalid parameters BadParameters(String), /// The chain id is not supported InvalidChainId, /// The simulation failed - SimulationError { - #[schema(value_type=String)] - result: Bytes, - reason: String, - }, + SimulationError { result: Bytes, reason: String }, /// The order was not found OpportunityNotFound, /// The server cannot currently communicate with the blockchain, so is not able to verify @@ -107,37 +103,40 @@ pub enum RestError { Unknown, } +#[derive(ToResponse, ToSchema, Serialize)] +#[response(description = "An error occurred processing the request")] +struct ErrorBodyResponse { + error: String, +} + impl IntoResponse for RestError { fn into_response(self) -> Response { - match self { + let (status, msg) = match self { RestError::BadParameters(msg) => { - (StatusCode::BAD_REQUEST, format!("Bad parameters: {}", msg)).into_response() - } - RestError::InvalidChainId => { - (StatusCode::BAD_REQUEST, "The chain id is not supported").into_response() + (StatusCode::BAD_REQUEST, format!("Bad parameters: {}", msg)) } + RestError::InvalidChainId => ( + StatusCode::NOT_FOUND, + "The chain id is not found".to_string(), + ), RestError::SimulationError { result, reason } => ( StatusCode::BAD_REQUEST, format!("Simulation failed: {} ({})", result, reason), - ) - .into_response(), + ), RestError::OpportunityNotFound => ( StatusCode::NOT_FOUND, - "Order with the specified id was not found", - ) - .into_response(), - + "Order with the specified id was not found".to_string(), + ), RestError::TemporarilyUnavailable => ( StatusCode::SERVICE_UNAVAILABLE, - "This service is temporarily unavailable", - ) - .into_response(), + "This service is temporarily unavailable".to_string(), + ), RestError::Unknown => ( StatusCode::INTERNAL_SERVER_ERROR, - "An unknown error occurred processing the request", - ) - .into_response(), - } + "An unknown error occurred processing the request".to_string(), + ), + }; + (status, Json(ErrorBodyResponse { error: msg })).into_response() } } @@ -152,13 +151,18 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> { #[derive(OpenApi)] #[openapi( paths( - rest::bid, - marketplace::submit_opportunity, - marketplace::bid_opportunity, - marketplace::fetch_opportunities, + bid::bid, + liquidation::submit_opportunity, + liquidation::bid_opportunity, + liquidation::fetch_opportunities, ), components( - schemas(Bid),schemas(LiquidationOpportunity),schemas(OpportunityBid), schemas(TokenQty),responses(RestError) + schemas(Bid), + schemas(LiquidationOpportunity), + schemas(OpportunityBid), + schemas(TokenQty), + schemas(ErrorBodyResponse), + responses(ErrorBodyResponse) ), tags( (name = "PER Auction", description = "Pyth Express Relay Auction Server") diff --git a/auction-server/src/api/bid.rs b/auction-server/src/api/bid.rs index 726931fc..55d1b5fb 100644 --- a/auction-server/src/api/bid.rs +++ b/auction-server/src/api/bid.rs @@ -1,6 +1,9 @@ use { crate::{ - api::RestError, + api::{ + ErrorBodyResponse, + RestError, + }, auction::{ simulate_bids, SimulationError, @@ -105,7 +108,8 @@ pub async fn handle_bid(store: Arc, bid: Bid) -> Result>, diff --git a/auction-server/src/api/liquidation.rs b/auction-server/src/api/liquidation.rs index 262904ae..8a109216 100644 --- a/auction-server/src/api/liquidation.rs +++ b/auction-server/src/api/liquidation.rs @@ -2,6 +2,7 @@ use { crate::{ api::{ bid::handle_bid, + ErrorBodyResponse, RestError, }, config::ChainId, @@ -105,7 +106,8 @@ fn parse_tokens(tokens: Vec) -> Vec<(Address, U256)> { /// and will be available for bidding. #[utoipa::path(post, path = "/v1/liquidation/submit_opportunity", request_body = LiquidationOpportunity, responses( (status = 200, description = "Opportunity was stored succesfuly with the returned uuid", body = String), - (status = 400, response=RestError) + (status = 400, response = ErrorBodyResponse), + (status = 404, description = "Chain id was not found", body = ErrorBodyResponse), ),)] pub async fn submit_opportunity( State(store): State>, @@ -142,7 +144,8 @@ pub async fn submit_opportunity( /// Fetch all liquidation opportunities ready to be exectued. #[utoipa::path(get, path = "/v1/liquidation/fetch_opportunities", responses( (status = 200, description = "Array of liquidation opportunities ready for bidding", body = Vec), - (status = 400, response=RestError) + (status = 400, response = ErrorBodyResponse), + (status = 404, description = "Chain id was not found", body = ErrorBodyResponse), ),)] pub async fn fetch_opportunities( State(store): State>, @@ -209,7 +212,8 @@ pub struct OpportunityBid { /// Bid on liquidation opportunity #[utoipa::path(post, path = "/v1/liquidation/bid_opportunity", request_body=OpportunityBid, responses( (status = 200, description = "Bid Result", body = String), - (status = 400, response=RestError) + (status = 400, response = ErrorBodyResponse), + (status = 404, description = "Opportunity or chain id was not found", body = ErrorBodyResponse), ),)] pub async fn bid_opportunity( State(store): State>, From 3b9476ef53464c1b0b6d59b124453b370028d568 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Tue, 6 Feb 2024 15:15:53 +0100 Subject: [PATCH 04/10] Add params for fetch opportunities --- auction-server/src/api/bid.rs | 2 +- auction-server/src/api/liquidation.rs | 30 +++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/auction-server/src/api/bid.rs b/auction-server/src/api/bid.rs index 55d1b5fb..3b223940 100644 --- a/auction-server/src/api/bid.rs +++ b/auction-server/src/api/bid.rs @@ -41,7 +41,7 @@ pub struct Bid { #[schema(example = "0xdeadbeef", value_type=String)] pub permission_key: Bytes, /// The chain id to bid on. - #[schema(example = "sepolia")] + #[schema(example = "sepolia", value_type=String)] pub chain_id: String, /// The contract address to call. #[schema(example = "0xcA11bde05977b3631167028862bE2a173976CA11",value_type = String)] diff --git a/auction-server/src/api/liquidation.rs b/auction-server/src/api/liquidation.rs index 8a109216..22626ac4 100644 --- a/auction-server/src/api/liquidation.rs +++ b/auction-server/src/api/liquidation.rs @@ -13,7 +13,10 @@ use { state::Store, }, axum::{ - extract::State, + extract::{ + Query, + State, + }, Json, }, ethers::{ @@ -29,7 +32,10 @@ use { Serialize, }, std::sync::Arc, - utoipa::ToSchema, + utoipa::{ + IntoParams, + ToSchema, + }, uuid::Uuid, }; @@ -54,7 +60,7 @@ pub struct LiquidationOpportunity { #[schema(example = "0xdeadbeefcafe", value_type=String)] permission_key: Bytes, /// The chain id where the liquidation will be executed. - #[schema(example = "sepolia")] + #[schema(example = "sepolia", value_type=String)] chain_id: ChainId, /// The contract address to call for execution of the liquidation. #[schema(example = "0xcA11bde05977b3631167028862bE2a173976CA11", value_type=String)] @@ -141,14 +147,23 @@ pub async fn submit_opportunity( Ok(id.to_string()) } + +#[derive(Serialize, Deserialize, IntoParams)] +pub struct ChainIdQueryParams { + #[param(example = "sepolia", value_type=Option)] + chain_id: Option, +} + /// Fetch all liquidation opportunities ready to be exectued. #[utoipa::path(get, path = "/v1/liquidation/fetch_opportunities", responses( (status = 200, description = "Array of liquidation opportunities ready for bidding", body = Vec), (status = 400, response = ErrorBodyResponse), (status = 404, description = "Chain id was not found", body = ErrorBodyResponse), -),)] +), +params(ChainIdQueryParams))] pub async fn fetch_opportunities( State(store): State>, + params: Query, ) -> Result>, RestError> { let opportunities: Vec = store .liquidation_store @@ -177,6 +192,13 @@ pub async fn fetch_opportunities( .collect(), }, }) + .filter(|opportunity| { + if let Some(chain_id) = ¶ms.chain_id { + opportunity.opportunity.chain_id == *chain_id + } else { + true + } + }) .collect(); Ok(opportunities.into()) From b6d54e92446e76522dc85c7099cc36aebfd4f73c Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Tue, 6 Feb 2024 15:31:16 +0100 Subject: [PATCH 05/10] Restful endpoint names --- auction-server/src/api.rs | 19 ++++++++----------- auction-server/src/api/liquidation.rs | 12 ++++++------ per_sdk/utils/endpoints.py | 11 +++-------- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/auction-server/src/api.rs b/auction-server/src/api.rs index 87d22f9c..b9dee624 100644 --- a/auction-server/src/api.rs +++ b/auction-server/src/api.rs @@ -152,9 +152,9 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> { #[openapi( paths( bid::bid, - liquidation::submit_opportunity, - liquidation::bid_opportunity, - liquidation::fetch_opportunities, + liquidation::post_opportunity, + liquidation::post_bid, + liquidation::get_opportunities, ), components( schemas(Bid), @@ -224,17 +224,14 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> { .route("/", get(root)) .route("/v1/bid", post(bid::bid)) .route( - "/v1/liquidation/submit_opportunity", - post(liquidation::submit_opportunity), + "/v1/liquidation/opportunity", + post(liquidation::post_opportunity), ) .route( - "/v1/liquidation/fetch_opportunities", - get(liquidation::fetch_opportunities), - ) - .route( - "/v1/liquidation/bid_opportunity", - post(liquidation::bid_opportunity), + "/v1/liquidation/opportunities", + get(liquidation::get_opportunities), ) + .route("/v1/liquidation/bid", post(liquidation::post_bid)) .layer(CorsLayer::permissive()) .with_state(server_store); diff --git a/auction-server/src/api/liquidation.rs b/auction-server/src/api/liquidation.rs index 22626ac4..19accdd5 100644 --- a/auction-server/src/api/liquidation.rs +++ b/auction-server/src/api/liquidation.rs @@ -110,12 +110,12 @@ fn parse_tokens(tokens: Vec) -> Vec<(Address, U256)> { /// /// The opportunity will be verified by the server. If the opportunity is valid, it will be stored in the database /// and will be available for bidding. -#[utoipa::path(post, path = "/v1/liquidation/submit_opportunity", request_body = LiquidationOpportunity, responses( +#[utoipa::path(post, path = "/v1/liquidation/opportunity", request_body = LiquidationOpportunity, responses( (status = 200, description = "Opportunity was stored succesfuly with the returned uuid", body = String), (status = 400, response = ErrorBodyResponse), (status = 404, description = "Chain id was not found", body = ErrorBodyResponse), ),)] -pub async fn submit_opportunity( +pub async fn post_opportunity( State(store): State>, Json(opportunity): Json, ) -> Result { @@ -155,13 +155,13 @@ pub struct ChainIdQueryParams { } /// Fetch all liquidation opportunities ready to be exectued. -#[utoipa::path(get, path = "/v1/liquidation/fetch_opportunities", responses( +#[utoipa::path(get, path = "/v1/liquidation/opportunities", responses( (status = 200, description = "Array of liquidation opportunities ready for bidding", body = Vec), (status = 400, response = ErrorBodyResponse), (status = 404, description = "Chain id was not found", body = ErrorBodyResponse), ), params(ChainIdQueryParams))] -pub async fn fetch_opportunities( +pub async fn get_opportunities( State(store): State>, params: Query, ) -> Result>, RestError> { @@ -232,12 +232,12 @@ pub struct OpportunityBid { } /// Bid on liquidation opportunity -#[utoipa::path(post, path = "/v1/liquidation/bid_opportunity", request_body=OpportunityBid, responses( +#[utoipa::path(post, path = "/v1/liquidation/bid", request_body=OpportunityBid, responses( (status = 200, description = "Bid Result", body = String), (status = 400, response = ErrorBodyResponse), (status = 404, description = "Opportunity or chain id was not found", body = ErrorBodyResponse), ),)] -pub async fn bid_opportunity( +pub async fn post_bid( State(store): State>, Json(opportunity_bid): Json, ) -> Result { diff --git a/per_sdk/utils/endpoints.py b/per_sdk/utils/endpoints.py index 9b1fa2b3..618f2221 100644 --- a/per_sdk/utils/endpoints.py +++ b/per_sdk/utils/endpoints.py @@ -1,12 +1,7 @@ -LIQUIDATION_SERVER_ENDPOINT = "http://localhost:9000" +LIQUIDATION_SERVER_ENDPOINT = "http://localhost:9000/v1" AUCTION_SERVER_ENDPOINT = "http://localhost:9000/bid" -LIQUIDATION_SERVER_ENDPOINT_SURFACE = ( - f"{LIQUIDATION_SERVER_ENDPOINT}/liquidation/submit_opportunity" -) LIQUIDATION_SERVER_ENDPOINT_GETOPPS = ( - f"{LIQUIDATION_SERVER_ENDPOINT}/liquidation/fetch_opportunities" -) -LIQUIDATION_SERVER_ENDPOINT_BID = ( - f"{LIQUIDATION_SERVER_ENDPOINT}/liquidation/bid_opportunity" + f"{LIQUIDATION_SERVER_ENDPOINT}/liquidation/opportunities" ) +LIQUIDATION_SERVER_ENDPOINT_BID = f"{LIQUIDATION_SERVER_ENDPOINT}/liquidation/bid" From 264d754937c8604a6ab76cb79edd38f850b2267f Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Tue, 6 Feb 2024 17:16:16 +0100 Subject: [PATCH 06/10] Start versioning --- auction-server/src/api.rs | 2 ++ auction-server/src/api/liquidation.rs | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/auction-server/src/api.rs b/auction-server/src/api.rs index b9dee624..13558cd3 100644 --- a/auction-server/src/api.rs +++ b/auction-server/src/api.rs @@ -6,6 +6,7 @@ use { LiquidationOpportunity, OpportunityBid, TokenQty, + VersionedLiquidationOpportunity, }, }, auction::run_submission_loop, @@ -160,6 +161,7 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> { schemas(Bid), schemas(LiquidationOpportunity), schemas(OpportunityBid), + schemas(VersionedLiquidationOpportunity), schemas(TokenQty), schemas(ErrorBodyResponse), responses(ErrorBodyResponse) diff --git a/auction-server/src/api/liquidation.rs b/auction-server/src/api/liquidation.rs index 19accdd5..d1de0b59 100644 --- a/auction-server/src/api/liquidation.rs +++ b/auction-server/src/api/liquidation.rs @@ -77,6 +77,13 @@ pub struct LiquidationOpportunity { receipt_tokens: Vec, } +#[derive(Serialize, Deserialize, ToSchema, Clone)] +#[serde(tag = "version")] +pub enum VersionedLiquidationOpportunity { + #[serde(rename = "v1")] + V1(LiquidationOpportunity), +} + /// Similar to LiquidationOpportunity, but with the opportunity id included. #[derive(Serialize, Deserialize, ToSchema, Clone)] pub struct LiquidationOpportunityWithId { @@ -110,15 +117,18 @@ fn parse_tokens(tokens: Vec) -> Vec<(Address, U256)> { /// /// The opportunity will be verified by the server. If the opportunity is valid, it will be stored in the database /// and will be available for bidding. -#[utoipa::path(post, path = "/v1/liquidation/opportunity", request_body = LiquidationOpportunity, responses( +#[utoipa::path(post, path = "/v1/liquidation/opportunity", request_body = VersionedLiquidationOpportunity, responses( (status = 200, description = "Opportunity was stored succesfuly with the returned uuid", body = String), (status = 400, response = ErrorBodyResponse), (status = 404, description = "Chain id was not found", body = ErrorBodyResponse), ),)] pub async fn post_opportunity( State(store): State>, - Json(opportunity): Json, + Json(opportunity): Json, ) -> Result { + let opportunity = match opportunity { + VersionedLiquidationOpportunity::V1(opportunity) => opportunity, + }; store .chains .get(&opportunity.chain_id) From 21fde4411aa1fbece8b58717a9702a705f1759b3 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Tue, 6 Feb 2024 18:36:05 +0100 Subject: [PATCH 07/10] Better naming and structure of liquidation objects + complete versioning --- auction-server/src/api.rs | 12 +- auction-server/src/api/liquidation.rs | 164 ++++++--------------- auction-server/src/liquidation_adapter.rs | 41 ++++-- auction-server/src/state.rs | 65 ++++++-- per_sdk/protocols/token_vault_monitor.py | 9 +- per_sdk/searcher/simple_searcher.py | 7 + per_sdk/utils/types_liquidation_adapter.py | 2 + 7 files changed, 145 insertions(+), 155 deletions(-) diff --git a/auction-server/src/api.rs b/auction-server/src/api.rs index f3b2a197..500f5f70 100644 --- a/auction-server/src/api.rs +++ b/auction-server/src/api.rs @@ -3,10 +3,8 @@ use { api::{ bid::Bid, liquidation::{ - LiquidationOpportunity, OpportunityBid, - TokenQty, - VersionedLiquidationOpportunity, + OpportunityParamsWithId, }, }, auction::run_submission_loop, @@ -19,7 +17,10 @@ use { state::{ ChainStore, LiquidationStore, + OpportunityParams, + OpportunityParamsV1, Store, + TokenQty, }, }, anyhow::{ @@ -166,9 +167,10 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> { ), components( schemas(Bid), - schemas(LiquidationOpportunity), + schemas(OpportunityParamsV1), schemas(OpportunityBid), - schemas(VersionedLiquidationOpportunity), + schemas(OpportunityParams), + schemas(OpportunityParamsWithId), schemas(TokenQty), schemas(ErrorBodyResponse), responses(ErrorBodyResponse) diff --git a/auction-server/src/api/liquidation.rs b/auction-server/src/api/liquidation.rs index eefaa515..da6fa572 100644 --- a/auction-server/src/api/liquidation.rs +++ b/auction-server/src/api/liquidation.rs @@ -12,9 +12,10 @@ use { verify_opportunity, }, state::{ + LiquidationOpportunity, + OpportunityParams, Store, UnixTimestamp, - VerifiedLiquidationOpportunity, }, }, axum::{ @@ -51,136 +52,62 @@ use { uuid::Uuid, }; -#[derive(Serialize, Deserialize, ToSchema, Clone)] -pub struct TokenQty { - /// Token contract address - #[schema(example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",value_type=String)] - contract: Address, - /// Token amount - #[schema(example = "1000", value_type=String)] - #[serde(with = "crate::serde::u256")] - amount: U256, -} - -/// A liquidation opportunity ready to be executed. -/// If a searcher signs the opportunity and have approved enough tokens to liquidation adapter, -/// by calling this contract with the given calldata and structures, they will receive the tokens specified -/// in the receipt_tokens field, and will send the tokens specified in the repay_tokens field. -#[derive(Serialize, Deserialize, ToSchema, Clone)] -pub struct LiquidationOpportunity { - /// The permission key required for succesful execution of the liquidation. - #[schema(example = "0xdeadbeefcafe", value_type=String)] - permission_key: Bytes, - /// The chain id where the liquidation will be executed. - #[schema(example = "sepolia", value_type=String)] - chain_id: ChainId, - /// The contract address to call for execution of the liquidation. - #[schema(example = "0xcA11bde05977b3631167028862bE2a173976CA11", value_type=String)] - contract: Address, - /// Calldata for the contract call. - #[schema(example = "0xdeadbeef", value_type=String)] - calldata: Bytes, - /// The value to send with the contract call. - #[schema(example = "1", value_type=String)] - #[serde(with = "crate::serde::u256")] - value: U256, - - repay_tokens: Vec, - receipt_tokens: Vec, -} +/// Similar to OpportunityParams, but with the opportunity id included. #[derive(Serialize, Deserialize, ToSchema, Clone)] -#[serde(tag = "version")] -pub enum VersionedLiquidationOpportunity { - #[serde(rename = "v1")] - V1(LiquidationOpportunity), -} - -/// Similar to LiquidationOpportunity, but with the opportunity id included. -#[derive(Serialize, Deserialize, ToSchema, Clone)] -pub struct LiquidationOpportunityWithId { +pub struct OpportunityParamsWithId { /// The opportunity unique id + #[schema(example = "f47ac10b-58cc-4372-a567-0e02b2c3d479", value_type=String)] opportunity_id: Uuid, /// opportunity data #[serde(flatten)] - opportunity: LiquidationOpportunity, -} - -impl From<(Address, U256)> for TokenQty { - fn from(token: (Address, U256)) -> Self { - TokenQty { - contract: token.0, - amount: token.1, - } - } -} - -impl From for (Address, U256) { - fn from(token: TokenQty) -> Self { - (token.contract, token.amount) - } -} - -fn parse_tokens(tokens: Vec) -> Vec<(Address, U256)> { - tokens.into_iter().map(|token| token.into()).collect() + params: OpportunityParams, } /// Submit a liquidation opportunity ready to be executed. /// /// The opportunity will be verified by the server. If the opportunity is valid, it will be stored in the database /// and will be available for bidding. -#[utoipa::path(post, path = "/v1/liquidation/opportunity", request_body = VersionedLiquidationOpportunity, responses( +#[utoipa::path(post, path = "/v1/liquidation/opportunity", request_body = OpportunityParams, responses( (status = 200, description = "Opportunity was stored succesfuly with the returned uuid", body = String), (status = 400, response = ErrorBodyResponse), (status = 404, description = "Chain id was not found", body = ErrorBodyResponse), ),)] pub async fn post_opportunity( State(store): State>, - Json(opportunity): Json, + Json(versioned_params): Json, ) -> Result { - let opportunity = match opportunity { - VersionedLiquidationOpportunity::V1(opportunity) => opportunity, + let params = match versioned_params.clone() { + OpportunityParams::V1(params) => params, }; let chain_store = store .chains - .get(&opportunity.chain_id) + .get(¶ms.chain_id) .ok_or(RestError::InvalidChainId)?; - let repay_tokens = parse_tokens(opportunity.repay_tokens); - let receipt_tokens = parse_tokens(opportunity.receipt_tokens); - let id = Uuid::new_v4(); - let verified_opportunity = VerifiedLiquidationOpportunity { + let opportunity = LiquidationOpportunity { id, creation_time: SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|_| RestError::BadParameters("Invalid system time".to_string()))? .as_secs() as UnixTimestamp, - chain_id: opportunity.chain_id.clone(), - permission_key: opportunity.permission_key.clone(), - contract: opportunity.contract, - calldata: opportunity.calldata, - value: opportunity.value, - repay_tokens, - receipt_tokens, + params: versioned_params, bidders: Default::default(), }; - verify_opportunity( - verified_opportunity.clone(), - chain_store, - store.per_operator.address(), - ) - .await - .map_err(|e| RestError::InvalidOpportunity(e.to_string()))?; + verify_opportunity(params.clone(), chain_store, store.per_operator.address()) + .await + .map_err(|e| RestError::InvalidOpportunity(e.to_string()))?; store .liquidation_store .opportunities .write() .await - .insert(opportunity.permission_key.clone(), verified_opportunity); + .insert(params.permission_key.clone(), opportunity); + //TODO: return json Ok(id.to_string()) } @@ -193,45 +120,32 @@ pub struct ChainIdQueryParams { /// Fetch all liquidation opportunities ready to be exectued. #[utoipa::path(get, path = "/v1/liquidation/opportunities", responses( - (status = 200, description = "Array of liquidation opportunities ready for bidding", body = Vec), + (status = 200, description = "Array of liquidation opportunities ready for bidding", body = Vec), (status = 400, response = ErrorBodyResponse), (status = 404, description = "Chain id was not found", body = ErrorBodyResponse), ), params(ChainIdQueryParams))] pub async fn get_opportunities( State(store): State>, - params: Query, -) -> Result>, RestError> { - let opportunities: Vec = store + query_params: Query, +) -> Result>, RestError> { + let opportunities: Vec = store .liquidation_store .opportunities .read() .await .values() .cloned() - .map(|opportunity| LiquidationOpportunityWithId { + .map(|opportunity| OpportunityParamsWithId { opportunity_id: opportunity.id, - opportunity: LiquidationOpportunity { - permission_key: opportunity.permission_key, - chain_id: opportunity.chain_id, - contract: opportunity.contract, - calldata: opportunity.calldata, - value: opportunity.value, - repay_tokens: opportunity - .repay_tokens - .into_iter() - .map(TokenQty::from) - .collect(), - receipt_tokens: opportunity - .receipt_tokens - .into_iter() - .map(TokenQty::from) - .collect(), - }, + params: opportunity.params, }) - .filter(|opportunity| { - if let Some(chain_id) = ¶ms.chain_id { - opportunity.opportunity.chain_id == *chain_id + .filter(|params_with_id| { + let params = match ¶ms_with_id.params { + OpportunityParams::V1(params) => params, + }; + if let Some(chain_id) = &query_params.chain_id { + params.chain_id == *chain_id } else { true } @@ -278,7 +192,7 @@ pub async fn post_bid( State(store): State>, Json(opportunity_bid): Json, ) -> Result { - let liquidation = store + let opportunity = store .liquidation_store .opportunities .read() @@ -288,26 +202,30 @@ pub async fn post_bid( .clone(); - if liquidation.id != opportunity_bid.opportunity_id { + if opportunity.id != opportunity_bid.opportunity_id { return Err(RestError::BadParameters( "Invalid opportunity_id".to_string(), )); } // TODO: move this logic to searcher side - if liquidation.bidders.contains(&opportunity_bid.liquidator) { + if opportunity.bidders.contains(&opportunity_bid.liquidator) { return Err(RestError::BadParameters( "Liquidator already bid on this opportunity".to_string(), )); } + let params = match &opportunity.params { + OpportunityParams::V1(params) => params, + }; + let chain_store = store .chains - .get(&liquidation.chain_id) + .get(¶ms.chain_id) .ok_or(RestError::InvalidChainId)?; let per_calldata = make_liquidator_calldata( - liquidation.clone(), + params.clone(), opportunity_bid.clone(), chain_store.provider.clone(), chain_store.config.adapter_contract, @@ -317,8 +235,8 @@ pub async fn post_bid( match handle_bid( store.clone(), crate::api::bid::Bid { - permission_key: liquidation.permission_key.clone(), - chain_id: liquidation.chain_id.clone(), + permission_key: params.permission_key.clone(), + chain_id: params.chain_id.clone(), contract: chain_store.config.adapter_contract, calldata: per_calldata, amount: opportunity_bid.amount, diff --git a/auction-server/src/liquidation_adapter.rs b/auction-server/src/liquidation_adapter.rs index 573a3c92..1fde7222 100644 --- a/auction-server/src/liquidation_adapter.rs +++ b/auction-server/src/liquidation_adapter.rs @@ -11,10 +11,12 @@ use { }, state::{ ChainStore, + LiquidationOpportunity, + OpportunityParams, + OpportunityParamsV1, SpoofInfo, Store, UnixTimestamp, - VerifiedLiquidationOpportunity, }, token_spoof, }, @@ -75,14 +77,6 @@ abigen!( abigen!(ERC20, "../per_multicall/out/ERC20.sol/ERC20.json"); abigen!(WETH9, "../per_multicall/out/WETH9.sol/WETH9.json"); -impl From<(Address, U256)> for TokenQty { - fn from(token: (Address, U256)) -> Self { - TokenQty { - token: token.0, - amount: token.1, - } - } -} pub enum VerificationResult { Success, @@ -94,14 +88,14 @@ pub enum VerificationResult { /// Returns Ok(VerificationResult) if the simulation is successful or if the tokens cannot be spoofed /// Returns Err if the simulation fails despite spoofing or if any other error occurs pub async fn verify_opportunity( - opportunity: VerifiedLiquidationOpportunity, + opportunity: OpportunityParamsV1, chain_store: &ChainStore, per_operator: Address, ) -> Result { let client = Arc::new(chain_store.provider.clone()); let fake_wallet = LocalWallet::new(&mut rand::thread_rng()); let mut fake_bid = OpportunityBid { - opportunity_id: opportunity.id, + opportunity_id: Default::default(), liquidator: fake_wallet.address(), valid_until: U256::max_value(), permission_key: opportunity.permission_key.clone(), @@ -139,7 +133,11 @@ pub async fn verify_opportunity( .tx; let mut state = spoof::State::default(); let token_spoof_info = chain_store.token_spoof_info.read().await.clone(); - for (token, amount) in opportunity.repay_tokens.into_iter() { + for crate::state::TokenQty { + contract: token, + amount, + } in opportunity.repay_tokens.into_iter() + { let spoof_info = match token_spoof_info.get(&token) { Some(info) => info.clone(), None => { @@ -245,8 +243,16 @@ pub fn parse_revert_error(revert: &Bytes) -> Option { apdapter_decoded.or(erc20_decoded) } +impl From for TokenQty { + fn from(token: crate::state::TokenQty) -> Self { + TokenQty { + token: token.contract, + amount: token.amount, + } + } +} pub fn make_liquidator_params( - opportunity: VerifiedLiquidationOpportunity, + opportunity: OpportunityParamsV1, bid: OpportunityBid, ) -> liquidation_adapter::LiquidationCallParams { liquidation_adapter::LiquidationCallParams { @@ -271,7 +277,7 @@ pub fn make_liquidator_params( } pub async fn make_liquidator_calldata( - opportunity: VerifiedLiquidationOpportunity, + opportunity: OpportunityParamsV1, bid: OpportunityBid, provider: Provider, adapter_contract: Address, @@ -300,9 +306,12 @@ const MAX_STALE_OPPORTUNITY_SECS: i64 = 60; /// * `opportunity`: opportunity to verify /// * `store`: server store async fn verify_with_store( - opportunity: VerifiedLiquidationOpportunity, + verified_opportunity: LiquidationOpportunity, store: &Store, ) -> Result<()> { + let opportunity = match verified_opportunity.params { + OpportunityParams::V1(opportunity) => opportunity, + }; let chain_store = store .chains .get(&opportunity.chain_id) @@ -313,7 +322,7 @@ async fn verify_with_store( Ok(VerificationResult::UnableToSpoof) => { let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as UnixTimestamp; - if current_time - opportunity.creation_time > MAX_STALE_OPPORTUNITY_SECS { + if current_time - verified_opportunity.creation_time > MAX_STALE_OPPORTUNITY_SECS { Err(anyhow!("Opportunity is stale and unverifiable")) } else { Ok(()) diff --git a/auction-server/src/state.rs b/auction-server/src/state.rs index c86f1236..6b44f540 100644 --- a/auction-server/src/state.rs +++ b/auction-server/src/state.rs @@ -15,11 +15,16 @@ use { U256, }, }, + serde::{ + Deserialize, + Serialize, + }, std::collections::{ HashMap, HashSet, }, tokio::sync::RwLock, + utoipa::ToSchema, uuid::Uuid, }; @@ -34,18 +39,58 @@ pub struct SimulatedBid { } pub type UnixTimestamp = i64; -#[derive(Clone)] -pub struct VerifiedLiquidationOpportunity { - pub id: Uuid, - pub creation_time: UnixTimestamp, + +#[derive(Serialize, Deserialize, ToSchema, Clone)] +pub struct TokenQty { + /// Token contract address + #[schema(example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",value_type=String)] + pub contract: ethers::abi::Address, + /// Token amount + #[schema(example = "1000", value_type=String)] + #[serde(with = "crate::serde::u256")] + pub amount: U256, +} + +/// Opportunity parameters needed for on-chain execution +/// If a searcher signs the opportunity and have approved enough tokens to liquidation adapter, +/// by calling this contract with the given calldata and structures, they will receive the tokens specified +/// in the receipt_tokens field, and will send the tokens specified in the repay_tokens field. +#[derive(Serialize, Deserialize, ToSchema, Clone)] +pub struct OpportunityParamsV1 { + /// The permission key required for succesful execution of the liquidation. + #[schema(example = "0xdeadbeefcafe", value_type=String)] + pub permission_key: Bytes, + /// The chain id where the liquidation will be executed. + #[schema(example = "sepolia", value_type=String)] pub chain_id: ChainId, - pub permission_key: PermissionKey, - pub contract: Address, + /// The contract address to call for execution of the liquidation. + #[schema(example = "0xcA11bde05977b3631167028862bE2a173976CA11", value_type=String)] + pub contract: ethers::abi::Address, + /// Calldata for the contract call. + #[schema(example = "0xdeadbeef", value_type=String)] pub calldata: Bytes, + /// The value to send with the contract call. + #[schema(example = "1", value_type=String)] + #[serde(with = "crate::serde::u256")] pub value: U256, - pub repay_tokens: Vec<(Address, U256)>, - pub receipt_tokens: Vec<(Address, U256)>, - pub bidders: HashSet
, + + pub repay_tokens: Vec, + pub receipt_tokens: Vec, +} + +#[derive(Serialize, Deserialize, ToSchema, Clone)] +#[serde(tag = "version")] +pub enum OpportunityParams { + #[serde(rename = "v1")] + V1(OpportunityParamsV1), +} + +#[derive(Clone)] +pub struct LiquidationOpportunity { + pub id: Uuid, + pub creation_time: UnixTimestamp, + pub params: OpportunityParams, + pub bidders: HashSet
, } #[derive(Clone)] @@ -67,7 +112,7 @@ pub struct ChainStore { #[derive(Default)] pub struct LiquidationStore { - pub opportunities: RwLock>, + pub opportunities: RwLock>, } pub struct Store { diff --git a/per_sdk/protocols/token_vault_monitor.py b/per_sdk/protocols/token_vault_monitor.py index 813ca7cf..b75e5437 100644 --- a/per_sdk/protocols/token_vault_monitor.py +++ b/per_sdk/protocols/token_vault_monitor.py @@ -3,6 +3,7 @@ import base64 import json import logging +import urllib.parse from typing import TypedDict import httpx @@ -154,6 +155,7 @@ def create_liquidation_opp( "receipt_tokens": [ (account["token_address_collateral"], str(account["amount_collateral"])) ], + "version": "v1", } # TODO: figure out best interface to show partial liquidation possibility? Is this even important? @@ -277,7 +279,12 @@ async def main(): client = httpx.AsyncClient() for opp in opportunities: try: - resp = await client.post(args.liquidation_server_url, json=opp) + resp = await client.post( + urllib.parse.urljoin( + args.liquidation_server_url, "/v1/liquidation/opportunity" + ), + json=opp, + ) except Exception as e: logger.error(f"Failed to post to liquidation server: {e}") await asyncio.sleep(1) diff --git a/per_sdk/searcher/simple_searcher.py b/per_sdk/searcher/simple_searcher.py index 0168b94d..60182bb3 100644 --- a/per_sdk/searcher/simple_searcher.py +++ b/per_sdk/searcher/simple_searcher.py @@ -146,6 +146,13 @@ async def main(): logger.debug("Found %d liquidation opportunities", len(accounts_liquidatable)) for liquidation_opp in accounts_liquidatable: + if liquidation_opp["version"] != "v1": + logger.warning( + "Opportunity %s has unsupported version %s", + liquidation_opp["opportunity_id"], + liquidation_opp["version"], + ) + continue bid_info = assess_liquidation_opportunity(args.bid, liquidation_opp) if bid_info is not None: diff --git a/per_sdk/utils/types_liquidation_adapter.py b/per_sdk/utils/types_liquidation_adapter.py index cfdbbbca..28b868a2 100644 --- a/per_sdk/utils/types_liquidation_adapter.py +++ b/per_sdk/utils/types_liquidation_adapter.py @@ -23,6 +23,8 @@ class LiquidationOpportunity(TypedDict): repay_tokens: list[TokenQty] # A list of tokens that ought to be received by the liquidator in exchange for the repay tokens. Each entry in the list is a tuple (token address, hex string of receipt amount) receipt_tokens: list[TokenQty] + # Opportunity format version, used to determine how to interpret the opportunity data + version: str class LiquidationAdapterCalldata(TypedDict): From 13600948104cc50d02c481fca4bffc855b0eb659 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Tue, 6 Feb 2024 19:11:08 +0100 Subject: [PATCH 08/10] Make all endpoints return json --- auction-server/src/api.rs | 10 +++++++-- auction-server/src/api/bid.rs | 26 +++++++++++++++++------ auction-server/src/api/liquidation.rs | 30 ++++++++++++++++++--------- 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/auction-server/src/api.rs b/auction-server/src/api.rs index 500f5f70..8c4ef43c 100644 --- a/auction-server/src/api.rs +++ b/auction-server/src/api.rs @@ -1,7 +1,10 @@ use { crate::{ api::{ - bid::Bid, + bid::{ + Bid, + BidResult, + }, liquidation::{ OpportunityBid, OpportunityParamsWithId, @@ -172,8 +175,11 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> { schemas(OpportunityParams), schemas(OpportunityParamsWithId), schemas(TokenQty), + schemas(BidResult), schemas(ErrorBodyResponse), - responses(ErrorBodyResponse) + responses(ErrorBodyResponse), + responses(OpportunityParamsWithId), + responses(BidResult) ), tags( (name = "PER Auction", description = "Pyth Express Relay Auction Server") diff --git a/auction-server/src/api/bid.rs b/auction-server/src/api/bid.rs index 3b223940..77b80af8 100644 --- a/auction-server/src/api/bid.rs +++ b/auction-server/src/api/bid.rs @@ -32,7 +32,10 @@ use { Serialize, }, std::sync::Arc, - utoipa::ToSchema, + utoipa::{ + ToResponse, + ToSchema, + }, }; #[derive(Serialize, Deserialize, ToSchema, Clone)] @@ -55,7 +58,7 @@ pub struct Bid { pub amount: U256, } -pub async fn handle_bid(store: Arc, bid: Bid) -> Result { +pub async fn handle_bid(store: Arc, bid: Bid) -> Result<(), RestError> { let chain_store = store .chains .get(&bid.chain_id) @@ -99,7 +102,12 @@ pub async fn handle_bid(store: Arc, bid: Bid) -> Result, bid: Bid) -> Result>, Json(bid): Json, -) -> Result { +) -> Result, RestError> { let bid = bid.clone(); store .chains .get(&bid.chain_id) .ok_or(RestError::InvalidChainId)?; - handle_bid(store, bid).await + match handle_bid(store, bid).await { + Ok(_) => Ok(BidResult { + status: "OK".to_string(), + } + .into()), + Err(e) => Err(e), + } } diff --git a/auction-server/src/api/liquidation.rs b/auction-server/src/api/liquidation.rs index da6fa572..dab1fdcd 100644 --- a/auction-server/src/api/liquidation.rs +++ b/auction-server/src/api/liquidation.rs @@ -1,7 +1,10 @@ use { crate::{ api::{ - bid::handle_bid, + bid::{ + handle_bid, + BidResult, + }, ErrorBodyResponse, RestError, }, @@ -47,6 +50,7 @@ use { }, utoipa::{ IntoParams, + ToResponse, ToSchema, }, uuid::Uuid, @@ -54,7 +58,7 @@ use { /// Similar to OpportunityParams, but with the opportunity id included. -#[derive(Serialize, Deserialize, ToSchema, Clone)] +#[derive(Serialize, Deserialize, ToSchema, Clone, ToResponse)] pub struct OpportunityParamsWithId { /// The opportunity unique id #[schema(example = "f47ac10b-58cc-4372-a567-0e02b2c3d479", value_type=String)] @@ -69,14 +73,14 @@ pub struct OpportunityParamsWithId { /// The opportunity will be verified by the server. If the opportunity is valid, it will be stored in the database /// and will be available for bidding. #[utoipa::path(post, path = "/v1/liquidation/opportunity", request_body = OpportunityParams, responses( - (status = 200, description = "Opportunity was stored succesfuly with the returned uuid", body = String), + (status = 200, description = "The created opportunity", body = OpportunityParamsWithId), (status = 400, response = ErrorBodyResponse), (status = 404, description = "Chain id was not found", body = ErrorBodyResponse), ),)] pub async fn post_opportunity( State(store): State>, Json(versioned_params): Json, -) -> Result { +) -> Result, RestError> { let params = match versioned_params.clone() { OpportunityParams::V1(params) => params, }; @@ -92,7 +96,7 @@ pub async fn post_opportunity( .duration_since(UNIX_EPOCH) .map_err(|_| RestError::BadParameters("Invalid system time".to_string()))? .as_secs() as UnixTimestamp, - params: versioned_params, + params: versioned_params.clone(), bidders: Default::default(), }; @@ -107,8 +111,11 @@ pub async fn post_opportunity( .await .insert(params.permission_key.clone(), opportunity); - //TODO: return json - Ok(id.to_string()) + Ok(OpportunityParamsWithId { + opportunity_id: id, + params: versioned_params, + } + .into()) } @@ -184,14 +191,14 @@ pub struct OpportunityBid { /// Bid on liquidation opportunity #[utoipa::path(post, path = "/v1/liquidation/bid", request_body=OpportunityBid, responses( - (status = 200, description = "Bid Result", body = String), + (status = 200, description = "Bid Result", body = BidResult, example = json!({"status": "OK"})), (status = 400, response = ErrorBodyResponse), (status = 404, description = "Opportunity or chain id was not found", body = ErrorBodyResponse), ),)] pub async fn post_bid( State(store): State>, Json(opportunity_bid): Json, -) -> Result { +) -> Result, RestError> { let opportunity = store .liquidation_store .opportunities @@ -250,7 +257,10 @@ pub async fn post_bid( if let Some(liquidation) = liquidation { liquidation.bidders.insert(opportunity_bid.liquidator); } - Ok("OK".to_string()) + Ok(BidResult { + status: "OK".to_string(), + } + .into()) } Err(e) => match e { RestError::SimulationError { result, reason } => { From 214fcb41e9ffd07cf92badc079531af84f320477 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Tue, 6 Feb 2024 19:29:02 +0100 Subject: [PATCH 09/10] Consistent variable naming --- auction-server/src/liquidation_adapter.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/auction-server/src/liquidation_adapter.rs b/auction-server/src/liquidation_adapter.rs index 1fde7222..47db7786 100644 --- a/auction-server/src/liquidation_adapter.rs +++ b/auction-server/src/liquidation_adapter.rs @@ -305,24 +305,21 @@ const MAX_STALE_OPPORTUNITY_SECS: i64 = 60; /// /// * `opportunity`: opportunity to verify /// * `store`: server store -async fn verify_with_store( - verified_opportunity: LiquidationOpportunity, - store: &Store, -) -> Result<()> { - let opportunity = match verified_opportunity.params { +async fn verify_with_store(opportunity: LiquidationOpportunity, store: &Store) -> Result<()> { + let params = match opportunity.params { OpportunityParams::V1(opportunity) => opportunity, }; let chain_store = store .chains - .get(&opportunity.chain_id) - .ok_or(anyhow!("Chain not found: {}", opportunity.chain_id))?; + .get(¶ms.chain_id) + .ok_or(anyhow!("Chain not found: {}", params.chain_id))?; let per_operator = store.per_operator.address(); - match verify_opportunity(opportunity.clone(), chain_store, per_operator).await { + match verify_opportunity(params.clone(), chain_store, per_operator).await { Ok(VerificationResult::Success) => Ok(()), Ok(VerificationResult::UnableToSpoof) => { let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as UnixTimestamp; - if current_time - verified_opportunity.creation_time > MAX_STALE_OPPORTUNITY_SECS { + if current_time - opportunity.creation_time > MAX_STALE_OPPORTUNITY_SECS { Err(anyhow!("Opportunity is stale and unverifiable")) } else { Ok(()) From 33f0ed1244d6c7e894b837ee6c6ea59fa4b7f3f6 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Wed, 7 Feb 2024 10:50:08 +0100 Subject: [PATCH 10/10] Address comments --- auction-server/src/api.rs | 11 +++++---- auction-server/src/api/bid.rs | 2 +- auction-server/src/api/liquidation.rs | 12 +++++----- auction-server/src/liquidation_adapter.rs | 1 - per_sdk/protocols/token_vault_monitor.py | 2 +- per_sdk/searcher/simple_searcher.py | 29 ++++++++++++++++------- per_sdk/utils/endpoints.py | 7 ------ 7 files changed, 35 insertions(+), 29 deletions(-) delete mode 100644 per_sdk/utils/endpoints.py diff --git a/auction-server/src/api.rs b/auction-server/src/api.rs index 8c4ef43c..d077f1cf 100644 --- a/auction-server/src/api.rs +++ b/auction-server/src/api.rs @@ -137,7 +137,7 @@ impl IntoResponse for RestError { ), RestError::OpportunityNotFound => ( StatusCode::NOT_FOUND, - "Order with the specified id was not found".to_string(), + "Opportunity with the specified id was not found".to_string(), ), RestError::TemporarilyUnavailable => ( StatusCode::SERVICE_UNAVAILABLE, @@ -241,16 +241,19 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> { let app: Router<()> = Router::new() .merge(SwaggerUi::new("/docs").url("/docs/openapi.json", ApiDoc::openapi())) .route("/", get(root)) - .route("/v1/bid", post(bid::bid)) + .route("/v1/bids", post(bid::bid)) .route( - "/v1/liquidation/opportunity", + "/v1/liquidation/opportunities", post(liquidation::post_opportunity), ) .route( "/v1/liquidation/opportunities", get(liquidation::get_opportunities), ) - .route("/v1/liquidation/bid", post(liquidation::post_bid)) + .route( + "/v1/liquidation/opportunities/:opportunity_id/bids", + post(liquidation::post_bid), + ) .layer(CorsLayer::permissive()) .with_state(server_store); diff --git a/auction-server/src/api/bid.rs b/auction-server/src/api/bid.rs index 77b80af8..427b2583 100644 --- a/auction-server/src/api/bid.rs +++ b/auction-server/src/api/bid.rs @@ -114,7 +114,7 @@ pub struct BidResult { /// /// Your bid will be simulated and verified by the server. Depending on the outcome of the auction, a transaction /// containing the contract call will be sent to the blockchain expecting the bid amount to be paid after the call. -#[utoipa::path(post, path = "/v1/bid", request_body = Bid, responses( +#[utoipa::path(post, path = "/v1/bids", request_body = Bid, responses( (status = 200, description = "Bid was placed succesfully", body = BidResult, example = json!({"status": "OK"})), (status = 400, response = ErrorBodyResponse), (status = 404, description = "Chain id was not found", body = ErrorBodyResponse), diff --git a/auction-server/src/api/liquidation.rs b/auction-server/src/api/liquidation.rs index dab1fdcd..ca77f6fa 100644 --- a/auction-server/src/api/liquidation.rs +++ b/auction-server/src/api/liquidation.rs @@ -23,6 +23,7 @@ use { }, axum::{ extract::{ + Path, Query, State, }, @@ -72,7 +73,7 @@ pub struct OpportunityParamsWithId { /// /// The opportunity will be verified by the server. If the opportunity is valid, it will be stored in the database /// and will be available for bidding. -#[utoipa::path(post, path = "/v1/liquidation/opportunity", request_body = OpportunityParams, responses( +#[utoipa::path(post, path = "/v1/liquidation/opportunities", request_body = OpportunityParams, responses( (status = 200, description = "The created opportunity", body = OpportunityParamsWithId), (status = 400, response = ErrorBodyResponse), (status = 404, description = "Chain id was not found", body = ErrorBodyResponse), @@ -164,9 +165,6 @@ pub async fn get_opportunities( #[derive(Serialize, Deserialize, ToSchema, Clone)] pub struct OpportunityBid { - /// The opportunity id to bid on. - #[schema(example = "f47ac10b-58cc-4372-a567-0e02b2c3d479",value_type=String)] - pub opportunity_id: Uuid, /// The opportunity permission key #[schema(example = "0xdeadbeefcafe", value_type=String)] pub permission_key: Bytes, @@ -190,13 +188,15 @@ pub struct OpportunityBid { } /// Bid on liquidation opportunity -#[utoipa::path(post, path = "/v1/liquidation/bid", request_body=OpportunityBid, responses( +#[utoipa::path(post, path = "/v1/liquidation/opportunities/{opportunity_id}/bids", request_body=OpportunityBid, + params(("opportunity_id", description = "Opportunity id to bid on")), responses( (status = 200, description = "Bid Result", body = BidResult, example = json!({"status": "OK"})), (status = 400, response = ErrorBodyResponse), (status = 404, description = "Opportunity or chain id was not found", body = ErrorBodyResponse), ),)] pub async fn post_bid( State(store): State>, + Path(opportunity_id): Path, Json(opportunity_bid): Json, ) -> Result, RestError> { let opportunity = store @@ -209,7 +209,7 @@ pub async fn post_bid( .clone(); - if opportunity.id != opportunity_bid.opportunity_id { + if opportunity.id != opportunity_id { return Err(RestError::BadParameters( "Invalid opportunity_id".to_string(), )); diff --git a/auction-server/src/liquidation_adapter.rs b/auction-server/src/liquidation_adapter.rs index 47db7786..886f5662 100644 --- a/auction-server/src/liquidation_adapter.rs +++ b/auction-server/src/liquidation_adapter.rs @@ -95,7 +95,6 @@ pub async fn verify_opportunity( let client = Arc::new(chain_store.provider.clone()); let fake_wallet = LocalWallet::new(&mut rand::thread_rng()); let mut fake_bid = OpportunityBid { - opportunity_id: Default::default(), liquidator: fake_wallet.address(), valid_until: U256::max_value(), permission_key: opportunity.permission_key.clone(), diff --git a/per_sdk/protocols/token_vault_monitor.py b/per_sdk/protocols/token_vault_monitor.py index b75e5437..42483253 100644 --- a/per_sdk/protocols/token_vault_monitor.py +++ b/per_sdk/protocols/token_vault_monitor.py @@ -281,7 +281,7 @@ async def main(): try: resp = await client.post( urllib.parse.urljoin( - args.liquidation_server_url, "/v1/liquidation/opportunity" + args.liquidation_server_url, "/v1/liquidation/opportunities" ), json=opp, ) diff --git a/per_sdk/searcher/simple_searcher.py b/per_sdk/searcher/simple_searcher.py index 60182bb3..e0f425a8 100644 --- a/per_sdk/searcher/simple_searcher.py +++ b/per_sdk/searcher/simple_searcher.py @@ -1,16 +1,13 @@ import argparse import asyncio import logging +import urllib.parse from typing import TypedDict import httpx from eth_account import Account from per_sdk.searcher.searcher_utils import BidInfo, construct_signature_liquidator -from per_sdk.utils.endpoints import ( - LIQUIDATION_SERVER_ENDPOINT_BID, - LIQUIDATION_SERVER_ENDPOINT_GETOPPS, -) from per_sdk.utils.types_liquidation_adapter import LiquidationOpportunity logger = logging.getLogger(__name__) @@ -116,6 +113,12 @@ async def main(): default=10, help="Default amount of bid for liquidation opportunities", ) + parser.add_argument( + "--liquidation-server-url", + type=str, + required=True, + help="Liquidation server endpoint to use for fetching opportunities and submitting bids", + ) args = parser.parse_args() logger.setLevel(logging.INFO if args.verbose == 0 else logging.DEBUG) @@ -135,7 +138,9 @@ async def main(): try: accounts_liquidatable = ( await client.get( - LIQUIDATION_SERVER_ENDPOINT_GETOPPS, + urllib.parse.urljoin( + args.liquidation_server_url, "/v1/liquidation/opportunities" + ), params={"chain_id": args.chain_id}, ) ).json() @@ -146,26 +151,32 @@ async def main(): logger.debug("Found %d liquidation opportunities", len(accounts_liquidatable)) for liquidation_opp in accounts_liquidatable: + opp_id = liquidation_opp["opportunity_id"] if liquidation_opp["version"] != "v1": logger.warning( "Opportunity %s has unsupported version %s", - liquidation_opp["opportunity_id"], + opp_id, liquidation_opp["version"], ) continue bid_info = assess_liquidation_opportunity(args.bid, liquidation_opp) if bid_info is not None: - tx = create_liquidation_transaction( liquidation_opp, sk_liquidator, bid_info ) - resp = await client.post(LIQUIDATION_SERVER_ENDPOINT_BID, json=tx) + resp = await client.post( + urllib.parse.urljoin( + args.liquidation_server_url, + f"/v1/liquidation/opportunities/{opp_id}/bids", + ), + json=tx, + ) logger.info( "Submitted bid amount %s for opportunity %s, server response: %s", bid_info["bid"], - liquidation_opp["opportunity_id"], + opp_id, resp.text, ) diff --git a/per_sdk/utils/endpoints.py b/per_sdk/utils/endpoints.py deleted file mode 100644 index 618f2221..00000000 --- a/per_sdk/utils/endpoints.py +++ /dev/null @@ -1,7 +0,0 @@ -LIQUIDATION_SERVER_ENDPOINT = "http://localhost:9000/v1" -AUCTION_SERVER_ENDPOINT = "http://localhost:9000/bid" - -LIQUIDATION_SERVER_ENDPOINT_GETOPPS = ( - f"{LIQUIDATION_SERVER_ENDPOINT}/liquidation/opportunities" -) -LIQUIDATION_SERVER_ENDPOINT_BID = f"{LIQUIDATION_SERVER_ENDPOINT}/liquidation/bid"