mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-23 17:38:06 +00:00
Merge pull request #55 from floor-licker/fix/prepared-order-path
feat: add prepared order path
This commit is contained in:
@@ -89,6 +89,31 @@ fn benchmark_create_order_eip712(c: &mut Criterion) {
|
|||||||
black_box(signed_order)
|
black_box(signed_order)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let prepared = builder
|
||||||
|
.prepare_order_path(
|
||||||
|
CHAIN_ID,
|
||||||
|
order_args.token_id.clone(),
|
||||||
|
options.tick_size.unwrap(),
|
||||||
|
options.neg_risk.unwrap(),
|
||||||
|
order_args.builder_code.as_deref(),
|
||||||
|
order_args.metadata.as_deref(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
c.bench_function("prepared_create_order_eip712_signature", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let signed_order = prepared
|
||||||
|
.create_limit_order(
|
||||||
|
black_box(order_args.side),
|
||||||
|
black_box(order_args.price),
|
||||||
|
black_box(order_args.size),
|
||||||
|
black_box(order_args.expiration),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
black_box(signed_order)
|
||||||
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Benchmark: Serialize a signed order body and build L2 auth headers for POST /order.
|
// Benchmark: Serialize a signed order body and build L2 auth headers for POST /order.
|
||||||
|
|||||||
+31
-9
@@ -8,7 +8,7 @@ use crate::types::ApiCredentials;
|
|||||||
use alloy_primitives::{hex::encode_prefixed, Address, B256, U256};
|
use alloy_primitives::{hex::encode_prefixed, Address, B256, U256};
|
||||||
use alloy_signer::SignerSync;
|
use alloy_signer::SignerSync;
|
||||||
use alloy_signer_local::PrivateKeySigner;
|
use alloy_signer_local::PrivateKeySigner;
|
||||||
use alloy_sol_types::{eip712_domain, sol};
|
use alloy_sol_types::{eip712_domain, sol, Eip712Domain};
|
||||||
use base64::engine::Engine;
|
use base64::engine::Engine;
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -142,6 +142,25 @@ pub struct SignedOrderMessage {
|
|||||||
pub builder: B256,
|
pub builder: B256,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepared EIP-712 domain for signing orders against one exchange contract.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PreparedOrderDomain {
|
||||||
|
domain: Eip712Domain,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreparedOrderDomain {
|
||||||
|
pub fn new(chain_id: u64, verifying_contract: Address) -> Self {
|
||||||
|
let domain = eip712_domain!(
|
||||||
|
name: "Polymarket CTF Exchange",
|
||||||
|
version: "2",
|
||||||
|
chain_id: chain_id,
|
||||||
|
verifying_contract: verifying_contract,
|
||||||
|
);
|
||||||
|
|
||||||
|
Self { domain }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get current Unix timestamp in seconds
|
/// Get current Unix timestamp in seconds
|
||||||
pub fn get_current_unix_time_secs() -> u64 {
|
pub fn get_current_unix_time_secs() -> u64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
@@ -185,6 +204,16 @@ pub fn sign_order_message(
|
|||||||
order: SignedOrderMessage,
|
order: SignedOrderMessage,
|
||||||
chain_id: u64,
|
chain_id: u64,
|
||||||
verifying_contract: Address,
|
verifying_contract: Address,
|
||||||
|
) -> Result<String> {
|
||||||
|
let domain = PreparedOrderDomain::new(chain_id, verifying_contract);
|
||||||
|
sign_order_message_with_domain(signer, order, &domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign order message using a prepared EIP-712 domain.
|
||||||
|
pub fn sign_order_message_with_domain(
|
||||||
|
signer: &PrivateKeySigner,
|
||||||
|
order: SignedOrderMessage,
|
||||||
|
domain: &PreparedOrderDomain,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let order = Order {
|
let order = Order {
|
||||||
salt: order.salt,
|
salt: order.salt,
|
||||||
@@ -200,15 +229,8 @@ pub fn sign_order_message(
|
|||||||
builder: order.builder,
|
builder: order.builder,
|
||||||
};
|
};
|
||||||
|
|
||||||
let domain = eip712_domain!(
|
|
||||||
name: "Polymarket CTF Exchange",
|
|
||||||
version: "2",
|
|
||||||
chain_id: chain_id,
|
|
||||||
verifying_contract: verifying_contract,
|
|
||||||
);
|
|
||||||
|
|
||||||
let signature = signer
|
let signature = signer
|
||||||
.sign_typed_data_sync(&order, &domain)
|
.sign_typed_data_sync(&order, &domain.domain)
|
||||||
.map_err(|e| PolyfillError::crypto(format!("Order signature failed: {}", e)))?;
|
.map_err(|e| PolyfillError::crypto(format!("Order signature failed: {}", e)))?;
|
||||||
|
|
||||||
Ok(encode_prefixed(signature.as_bytes()))
|
Ok(encode_prefixed(signature.as_bytes()))
|
||||||
|
|||||||
+83
-2
@@ -980,6 +980,35 @@ impl ClobClient {
|
|||||||
self.get_clob_market_info(&market.condition_id).await
|
self.get_clob_market_info(&market.condition_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepare reusable order-path state for repeated orders on one token.
|
||||||
|
///
|
||||||
|
/// This resolves tick-size/neg-risk once, then caches fixed order-path inputs such as the
|
||||||
|
/// parsed token ID, exchange address, normalized bytes32 values, and EIP-712 domain.
|
||||||
|
pub async fn prepare_order_path(
|
||||||
|
&self,
|
||||||
|
token_id: &str,
|
||||||
|
options: Option<&CreateOrderOptions>,
|
||||||
|
builder_code: Option<&str>,
|
||||||
|
metadata: Option<&str>,
|
||||||
|
) -> Result<crate::orders::PreparedOrderPath> {
|
||||||
|
let order_builder = self
|
||||||
|
.order_builder
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| PolyfillError::auth("Order builder not initialized"))?;
|
||||||
|
|
||||||
|
let create_order_options = self.get_filled_order_options(token_id, options).await?;
|
||||||
|
let builder_code = builder_code.or(self.builder_code.as_deref());
|
||||||
|
|
||||||
|
order_builder.prepare_order_path(
|
||||||
|
self.chain_id,
|
||||||
|
token_id,
|
||||||
|
create_order_options.tick_size.expect("Should be filled"),
|
||||||
|
create_order_options.neg_risk.expect("Should be filled"),
|
||||||
|
builder_code,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create an order
|
/// Create an order
|
||||||
pub async fn create_order(
|
pub async fn create_order(
|
||||||
&self,
|
&self,
|
||||||
@@ -2497,8 +2526,9 @@ pub type PolyfillClient = ClobClient;
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{ClobClient, OrderArgs as ClientOrderArgs};
|
use super::{ClobClient, OrderArgs as ClientOrderArgs};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
OrderType, PostOrderOptions, PricesHistoryInterval, RfqCreateQuote, RfqCreateRequest,
|
CreateOrderOptions, OrderType, PostOrderOptions, PricesHistoryInterval, RfqCreateQuote,
|
||||||
RfqOrderExecutionRequest, RfqQuotesParams, RfqRequestsParams, Side, SignedOrderRequest,
|
RfqCreateRequest, RfqOrderExecutionRequest, RfqQuotesParams, RfqRequestsParams, Side,
|
||||||
|
SignedOrderRequest,
|
||||||
};
|
};
|
||||||
use crate::{ApiCredentials, ClientConfig, PolyfillError};
|
use crate::{ApiCredentials, ClientConfig, PolyfillError};
|
||||||
use mockito::{Matcher, Server};
|
use mockito::{Matcher, Server};
|
||||||
@@ -3269,6 +3299,57 @@ mod tests {
|
|||||||
assert_eq!(default_args.side, Side::BUY);
|
assert_eq!(default_args.side, Side::BUY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_prepare_order_path_uses_client_builder_code() {
|
||||||
|
let mut server = Server::new_async().await;
|
||||||
|
let tick_size_mock = server
|
||||||
|
.mock("GET", "/tick-size")
|
||||||
|
.match_query(Matcher::UrlEncoded("token_id".into(), "123456".into()))
|
||||||
|
.with_status(200)
|
||||||
|
.with_header("content-type", "application/json")
|
||||||
|
.with_body(r#"{"minimum_tick_size":"0.01"}"#)
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let client = ClobClient::from_config(ClientConfig {
|
||||||
|
base_url: server.url(),
|
||||||
|
chain: 137,
|
||||||
|
private_key: Some(
|
||||||
|
"0x1234567890123456789012345678901234567890123456789012345678901234".to_string(),
|
||||||
|
),
|
||||||
|
builder_code: Some(
|
||||||
|
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
|
||||||
|
),
|
||||||
|
..ClientConfig::default()
|
||||||
|
})
|
||||||
|
.expect("test auth client with builder code");
|
||||||
|
let options = CreateOrderOptions {
|
||||||
|
tick_size: Some(Decimal::from_str("0.01").unwrap()),
|
||||||
|
neg_risk: Some(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let prepared = client
|
||||||
|
.prepare_order_path("123456", Some(&options), None, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let order = prepared
|
||||||
|
.create_limit_order(
|
||||||
|
Side::BUY,
|
||||||
|
Decimal::from_str("0.45").unwrap(),
|
||||||
|
Decimal::from_str("12.34").unwrap(),
|
||||||
|
Some(1_900_000_000),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
tick_size_mock.assert_async().await;
|
||||||
|
assert_eq!(order.token_id, "123456");
|
||||||
|
assert_eq!(
|
||||||
|
order.builder,
|
||||||
|
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
);
|
||||||
|
assert_eq!(order.metadata, crate::orders::BYTES32_ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_get_clob_market_info_success() {
|
async fn test_get_clob_market_info_success() {
|
||||||
let mut server = Server::new_async().await;
|
let mut server = Server::new_async().await;
|
||||||
|
|||||||
+251
-27
@@ -3,7 +3,9 @@
|
|||||||
//! This module handles the complex process of creating and signing orders
|
//! This module handles the complex process of creating and signing orders
|
||||||
//! for the Polymarket CLOB, including EIP-712 signature generation.
|
//! for the Polymarket CLOB, including EIP-712 signature generation.
|
||||||
|
|
||||||
use crate::auth::{sign_order_message, SignedOrderMessage};
|
use crate::auth::{
|
||||||
|
sign_order_message, sign_order_message_with_domain, PreparedOrderDomain, SignedOrderMessage,
|
||||||
|
};
|
||||||
use crate::errors::{PolyfillError, Result};
|
use crate::errors::{PolyfillError, Result};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
CreateOrderOptions, MarketOrderArgs, OrderArgs, OrderType, Side, SignedOrderRequest,
|
CreateOrderOptions, MarketOrderArgs, OrderArgs, OrderType, Side, SignedOrderRequest,
|
||||||
@@ -33,6 +35,7 @@ pub enum SigType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Rounding configuration for different tick sizes
|
/// Rounding configuration for different tick sizes
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
pub struct RoundConfig {
|
pub struct RoundConfig {
|
||||||
price: u32,
|
price: u32,
|
||||||
size: u32,
|
size: u32,
|
||||||
@@ -47,10 +50,28 @@ pub struct ContractConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Order builder for creating and signing orders
|
/// Order builder for creating and signing orders
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct OrderBuilder {
|
pub struct OrderBuilder {
|
||||||
signer: PrivateKeySigner,
|
signer: PrivateKeySigner,
|
||||||
|
signer_address: Address,
|
||||||
|
signer_checksum: String,
|
||||||
sig_type: SigType,
|
sig_type: SigType,
|
||||||
funder: Address,
|
funder: Address,
|
||||||
|
funder_checksum: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepared low-latency order path for a single market/token configuration.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PreparedOrderPath {
|
||||||
|
builder: OrderBuilder,
|
||||||
|
token_id: String,
|
||||||
|
token_id_u256: U256,
|
||||||
|
round_config: RoundConfig,
|
||||||
|
domain: PreparedOrderDomain,
|
||||||
|
builder_bytes: B256,
|
||||||
|
builder_code: String,
|
||||||
|
metadata_bytes: B256,
|
||||||
|
metadata: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
const POLYGON_PROXY_FACTORY: &str = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052";
|
const POLYGON_PROXY_FACTORY: &str = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052";
|
||||||
@@ -99,6 +120,28 @@ pub fn get_contract_config(chain_id: u64, neg_risk: bool) -> Option<ContractConf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn exchange_address_for(chain_id: u64, neg_risk: bool) -> Result<Address> {
|
||||||
|
let contract_config = get_contract_config(chain_id, neg_risk).ok_or_else(|| {
|
||||||
|
PolyfillError::config("No contract found with given chain_id and neg_risk")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Address::from_str(&contract_config.exchange)
|
||||||
|
.map_err(|e| PolyfillError::config(format!("Invalid exchange address: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_token_id(token_id: &str) -> Result<U256> {
|
||||||
|
U256::from_str_radix(token_id, 10)
|
||||||
|
.map_err(|e| PolyfillError::validation(format!("Incorrect tokenId format: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_optional_bytes32(name: &str, value: Option<&str>) -> Result<(B256, String)> {
|
||||||
|
let normalized = normalize_optional_bytes32(name, value)?;
|
||||||
|
let parsed = B256::from_str(&normalized)
|
||||||
|
.map_err(|e| PolyfillError::validation(format!("Invalid {name} bytes32 value: {e}")))?;
|
||||||
|
|
||||||
|
Ok((parsed, normalized))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sig_type_from_u8(signature_type: u8) -> Result<SigType> {
|
pub fn sig_type_from_u8(signature_type: u8) -> Result<SigType> {
|
||||||
match signature_type {
|
match signature_type {
|
||||||
0 => Ok(SigType::Eoa),
|
0 => Ok(SigType::Eoa),
|
||||||
@@ -303,12 +346,18 @@ impl OrderBuilder {
|
|||||||
funder: Option<Address>,
|
funder: Option<Address>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let sig_type = sig_type.unwrap_or(SigType::Eoa);
|
let sig_type = sig_type.unwrap_or(SigType::Eoa);
|
||||||
let funder = funder.unwrap_or(signer.address());
|
let signer_address = signer.address();
|
||||||
|
let signer_checksum = signer_address.to_checksum(None);
|
||||||
|
let funder = funder.unwrap_or(signer_address);
|
||||||
|
let funder_checksum = funder.to_checksum(None);
|
||||||
|
|
||||||
OrderBuilder {
|
OrderBuilder {
|
||||||
signer,
|
signer,
|
||||||
|
signer_address,
|
||||||
|
signer_checksum,
|
||||||
sig_type,
|
sig_type,
|
||||||
funder,
|
funder,
|
||||||
|
funder_checksum,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +366,41 @@ impl OrderBuilder {
|
|||||||
self.sig_type as u8
|
self.sig_type as u8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepare reusable order-path state for one market/token.
|
||||||
|
///
|
||||||
|
/// This caches tick-size rounding, exchange address parsing, token ID parsing, normalized
|
||||||
|
/// bytes32 fields, and the EIP-712 domain. Use this for latency-sensitive repeated order
|
||||||
|
/// creation on the same token.
|
||||||
|
pub fn prepare_order_path(
|
||||||
|
&self,
|
||||||
|
chain_id: u64,
|
||||||
|
token_id: impl Into<String>,
|
||||||
|
tick_size: Decimal,
|
||||||
|
neg_risk: bool,
|
||||||
|
builder_code: Option<&str>,
|
||||||
|
metadata: Option<&str>,
|
||||||
|
) -> Result<PreparedOrderPath> {
|
||||||
|
let token_id = token_id.into();
|
||||||
|
let token_id_u256 = parse_token_id(&token_id)?;
|
||||||
|
let round_config = *parse_round_config(tick_size)?;
|
||||||
|
let exchange = exchange_address_for(chain_id, neg_risk)?;
|
||||||
|
let domain = PreparedOrderDomain::new(chain_id, exchange);
|
||||||
|
let (builder_bytes, builder_code) = parse_optional_bytes32("builder_code", builder_code)?;
|
||||||
|
let (metadata_bytes, metadata) = parse_optional_bytes32("metadata", metadata)?;
|
||||||
|
|
||||||
|
Ok(PreparedOrderPath {
|
||||||
|
builder: self.clone(),
|
||||||
|
token_id,
|
||||||
|
token_id_u256,
|
||||||
|
round_config,
|
||||||
|
domain,
|
||||||
|
builder_bytes,
|
||||||
|
builder_code,
|
||||||
|
metadata_bytes,
|
||||||
|
metadata,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Fix amount rounding according to configuration
|
/// Fix amount rounding according to configuration
|
||||||
fn fix_amount_rounding(&self, mut amt: Decimal, round_config: &RoundConfig) -> Decimal {
|
fn fix_amount_rounding(&self, mut amt: Decimal, round_config: &RoundConfig) -> Decimal {
|
||||||
if amt.scale() > round_config.amount {
|
if amt.scale() > round_config.amount {
|
||||||
@@ -459,12 +543,7 @@ impl OrderBuilder {
|
|||||||
.neg_risk
|
.neg_risk
|
||||||
.ok_or_else(|| PolyfillError::validation("Cannot create order without neg_risk"))?;
|
.ok_or_else(|| PolyfillError::validation("Cannot create order without neg_risk"))?;
|
||||||
|
|
||||||
let contract_config = get_contract_config(chain_id, neg_risk).ok_or_else(|| {
|
let exchange_address = exchange_address_for(chain_id, neg_risk)?;
|
||||||
PolyfillError::config("No contract found with given chain_id and neg_risk")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let exchange_address = Address::from_str(&contract_config.exchange)
|
|
||||||
.map_err(|e| PolyfillError::config(format!("Invalid exchange address: {}", e)))?;
|
|
||||||
|
|
||||||
self.build_signed_order(
|
self.build_signed_order(
|
||||||
order_args.token_id.clone(),
|
order_args.token_id.clone(),
|
||||||
@@ -502,12 +581,7 @@ impl OrderBuilder {
|
|||||||
.neg_risk
|
.neg_risk
|
||||||
.ok_or_else(|| PolyfillError::validation("Cannot create order without neg_risk"))?;
|
.ok_or_else(|| PolyfillError::validation("Cannot create order without neg_risk"))?;
|
||||||
|
|
||||||
let contract_config = get_contract_config(chain_id, neg_risk).ok_or_else(|| {
|
let exchange_address = exchange_address_for(chain_id, neg_risk)?;
|
||||||
PolyfillError::config("No contract found with given chain_id and neg_risk")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let exchange_address = Address::from_str(&contract_config.exchange)
|
|
||||||
.map_err(|e| PolyfillError::config(format!("Invalid exchange address: {}", e)))?;
|
|
||||||
|
|
||||||
self.build_signed_order(
|
self.build_signed_order(
|
||||||
order_args.token_id.clone(),
|
order_args.token_id.clone(),
|
||||||
@@ -542,35 +616,30 @@ impl OrderBuilder {
|
|||||||
.expect("Time went backwards")
|
.expect("Time went backwards")
|
||||||
.as_millis();
|
.as_millis();
|
||||||
|
|
||||||
let u256_token_id = U256::from_str_radix(&token_id, 10)
|
let u256_token_id = parse_token_id(&token_id)?;
|
||||||
.map_err(|e| PolyfillError::validation(format!("Incorrect tokenId format: {}", e)))?;
|
let (builder_bytes, builder) = parse_optional_bytes32("builder_code", builder_code)?;
|
||||||
let builder = normalize_optional_bytes32("builder_code", builder_code)?;
|
let (metadata_bytes, metadata) = parse_optional_bytes32("metadata", metadata)?;
|
||||||
let metadata = normalize_optional_bytes32("metadata", metadata)?;
|
|
||||||
|
|
||||||
let order = SignedOrderMessage {
|
let order = SignedOrderMessage {
|
||||||
salt: U256::from(seed),
|
salt: U256::from(seed),
|
||||||
maker: self.funder,
|
maker: self.funder,
|
||||||
signer: self.signer.address(),
|
signer: self.signer_address,
|
||||||
token_id: u256_token_id,
|
token_id: u256_token_id,
|
||||||
maker_amount,
|
maker_amount,
|
||||||
taker_amount,
|
taker_amount,
|
||||||
side: side as u8,
|
side: side as u8,
|
||||||
signature_type: self.sig_type as u8,
|
signature_type: self.sig_type as u8,
|
||||||
timestamp: U256::from(timestamp),
|
timestamp: U256::from(timestamp),
|
||||||
metadata: B256::from_str(&metadata).map_err(|e| {
|
metadata: metadata_bytes,
|
||||||
PolyfillError::validation(format!("Invalid metadata bytes32 value: {e}"))
|
builder: builder_bytes,
|
||||||
})?,
|
|
||||||
builder: B256::from_str(&builder).map_err(|e| {
|
|
||||||
PolyfillError::validation(format!("Invalid builder_code bytes32 value: {e}"))
|
|
||||||
})?,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let signature = sign_order_message(&self.signer, order, chain_id, exchange)?;
|
let signature = sign_order_message(&self.signer, order, chain_id, exchange)?;
|
||||||
|
|
||||||
Ok(SignedOrderRequest {
|
Ok(SignedOrderRequest {
|
||||||
salt: seed,
|
salt: seed,
|
||||||
maker: self.funder.to_checksum(None),
|
maker: self.funder_checksum.clone(),
|
||||||
signer: self.signer.address().to_checksum(None),
|
signer: self.signer_checksum.clone(),
|
||||||
token_id,
|
token_id,
|
||||||
maker_amount: maker_amount.to_string(),
|
maker_amount: maker_amount.to_string(),
|
||||||
taker_amount: taker_amount.to_string(),
|
taker_amount: taker_amount.to_string(),
|
||||||
@@ -585,6 +654,90 @@ impl OrderBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PreparedOrderPath {
|
||||||
|
/// Create and sign a limit order using the cached market/token context.
|
||||||
|
pub fn create_limit_order(
|
||||||
|
&self,
|
||||||
|
side: Side,
|
||||||
|
price: Decimal,
|
||||||
|
size: Decimal,
|
||||||
|
expiration: Option<u64>,
|
||||||
|
) -> Result<SignedOrderRequest> {
|
||||||
|
let (maker_amount, taker_amount) =
|
||||||
|
self.builder
|
||||||
|
.get_order_amounts(side, size, price, &self.round_config)?;
|
||||||
|
|
||||||
|
self.build_signed_order(side, maker_amount, taker_amount, expiration.unwrap_or(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create and sign a market order using the cached market/token context.
|
||||||
|
pub fn create_market_order(
|
||||||
|
&self,
|
||||||
|
side: Side,
|
||||||
|
amount: Decimal,
|
||||||
|
price: Decimal,
|
||||||
|
order_type: OrderType,
|
||||||
|
) -> Result<SignedOrderRequest> {
|
||||||
|
if !matches!(order_type, OrderType::FOK | OrderType::FAK) {
|
||||||
|
return Err(PolyfillError::validation(
|
||||||
|
"Market orders only support FOK and FAK order types",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (maker_amount, taker_amount) =
|
||||||
|
self.builder
|
||||||
|
.get_market_order_amounts(side, amount, price, &self.round_config)?;
|
||||||
|
|
||||||
|
self.build_signed_order(side, maker_amount, taker_amount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_signed_order(
|
||||||
|
&self,
|
||||||
|
side: Side,
|
||||||
|
maker_amount: U256,
|
||||||
|
taker_amount: U256,
|
||||||
|
expiration: u64,
|
||||||
|
) -> Result<SignedOrderRequest> {
|
||||||
|
let seed = generate_seed();
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("Time went backwards")
|
||||||
|
.as_millis();
|
||||||
|
|
||||||
|
let order = SignedOrderMessage {
|
||||||
|
salt: U256::from(seed),
|
||||||
|
maker: self.builder.funder,
|
||||||
|
signer: self.builder.signer_address,
|
||||||
|
token_id: self.token_id_u256,
|
||||||
|
maker_amount,
|
||||||
|
taker_amount,
|
||||||
|
side: side as u8,
|
||||||
|
signature_type: self.builder.sig_type as u8,
|
||||||
|
timestamp: U256::from(timestamp),
|
||||||
|
metadata: self.metadata_bytes,
|
||||||
|
builder: self.builder_bytes,
|
||||||
|
};
|
||||||
|
|
||||||
|
let signature = sign_order_message_with_domain(&self.builder.signer, order, &self.domain)?;
|
||||||
|
|
||||||
|
Ok(SignedOrderRequest {
|
||||||
|
salt: seed,
|
||||||
|
maker: self.builder.funder_checksum.clone(),
|
||||||
|
signer: self.builder.signer_checksum.clone(),
|
||||||
|
token_id: self.token_id.clone(),
|
||||||
|
maker_amount: maker_amount.to_string(),
|
||||||
|
taker_amount: taker_amount.to_string(),
|
||||||
|
expiration: expiration.to_string(),
|
||||||
|
side: side.as_str().to_string(),
|
||||||
|
signature_type: self.builder.sig_type as u8,
|
||||||
|
timestamp: timestamp.to_string(),
|
||||||
|
metadata: self.metadata.clone(),
|
||||||
|
builder: self.builder_code.clone(),
|
||||||
|
signature,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -766,6 +919,77 @@ mod tests {
|
|||||||
assert_eq!(order.metadata, BYTES32_ZERO);
|
assert_eq!(order.metadata, BYTES32_ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prepared_order_path_creates_equivalent_limit_order_fields() {
|
||||||
|
let builder = test_builder();
|
||||||
|
let args = OrderArgs {
|
||||||
|
token_id: "123456".to_string(),
|
||||||
|
price: Decimal::from_str("0.45").unwrap(),
|
||||||
|
size: Decimal::from_str("12.34").unwrap(),
|
||||||
|
side: Side::BUY,
|
||||||
|
expiration: Some(1_900_000_000),
|
||||||
|
builder_code: Some(BYTES32_ZERO.to_string()),
|
||||||
|
metadata: None,
|
||||||
|
};
|
||||||
|
let options = CreateOrderOptions {
|
||||||
|
tick_size: Some(Decimal::from_str("0.01").unwrap()),
|
||||||
|
neg_risk: Some(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let order = builder.create_order(137, &args, &options).unwrap();
|
||||||
|
let prepared = builder
|
||||||
|
.prepare_order_path(
|
||||||
|
137,
|
||||||
|
args.token_id.clone(),
|
||||||
|
options.tick_size.unwrap(),
|
||||||
|
options.neg_risk.unwrap(),
|
||||||
|
args.builder_code.as_deref(),
|
||||||
|
args.metadata.as_deref(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let prepared_order = prepared
|
||||||
|
.create_limit_order(args.side, args.price, args.size, args.expiration)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(prepared_order.maker, order.maker);
|
||||||
|
assert_eq!(prepared_order.signer, order.signer);
|
||||||
|
assert_eq!(prepared_order.token_id, order.token_id);
|
||||||
|
assert_eq!(prepared_order.maker_amount, order.maker_amount);
|
||||||
|
assert_eq!(prepared_order.taker_amount, order.taker_amount);
|
||||||
|
assert_eq!(prepared_order.expiration, order.expiration);
|
||||||
|
assert_eq!(prepared_order.side, order.side);
|
||||||
|
assert_eq!(prepared_order.signature_type, order.signature_type);
|
||||||
|
assert_eq!(prepared_order.metadata, order.metadata);
|
||||||
|
assert_eq!(prepared_order.builder, order.builder);
|
||||||
|
assert!(prepared_order.signature.starts_with("0x"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prepared_market_order_rejects_unsupported_order_type() {
|
||||||
|
let builder = test_builder();
|
||||||
|
let prepared = builder
|
||||||
|
.prepare_order_path(
|
||||||
|
137,
|
||||||
|
"123456",
|
||||||
|
Decimal::from_str("0.01").unwrap(),
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = prepared
|
||||||
|
.create_market_order(
|
||||||
|
Side::BUY,
|
||||||
|
Decimal::from_str("10").unwrap(),
|
||||||
|
Decimal::from_str("0.45").unwrap(),
|
||||||
|
OrderType::GTC,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(err, PolyfillError::Validation { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_market_order_supports_fak() {
|
fn test_create_market_order_supports_fak() {
|
||||||
let builder = test_builder();
|
let builder = test_builder();
|
||||||
|
|||||||
Reference in New Issue
Block a user