use alloy_primitives::{Address, Bytes, U256, keccak256};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
use serde_with::{DisplayFromStr, serde_as};
use cowprotocol_primitives::order_id::{OrderClass, OrderUid};
pub use cowprotocol_primitives::{AppDataHash, EMPTY_APP_DATA_HASH, EMPTY_APP_DATA_JSON};
pub const LATEST_APP_DATA_VERSION: &str = "1.6.0";
pub const COW_RS_APP_CODE: &str = "cow-rs";
pub const COW_RS_WASM_APP_CODE: &str = "cow-rs-wasm";
pub const APP_DATA_SIZE_LIMIT: usize = 8192;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppDataDoc {
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub app_code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub environment: Option<String>,
#[serde(default)]
pub metadata: AppDataMetadata,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppDataMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quote: Option<AppDataQuote>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub order_class: Option<AppDataOrderClass>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub partner_fee: Option<AppDataPartnerFee>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub referrer: Option<AppDataReferrer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utm: Option<AppDataUtm>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hooks: Option<AppDataHooks>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flashloan: Option<AppDataFlashloan>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub replaced_order: Option<AppDataReplacedOrder>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub wrappers: Vec<AppDataWrapperCall>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppDataQuote {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slippage_bips: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppDataOrderClass {
pub order_class: OrderClass,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AppDataPartnerFee {
policy: FeePolicy,
recipient: Address,
}
impl Serialize for AppDataPartnerFee {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeMap as _;
let entry_count = match self.policy {
FeePolicy::Volume { .. } => 2,
FeePolicy::Surplus { .. } | FeePolicy::PriceImprovement { .. } => 3,
};
let mut map = serializer.serialize_map(Some(entry_count))?;
match self.policy {
FeePolicy::Volume { bps } => {
map.serialize_entry("bps", &bps)?;
}
FeePolicy::Surplus {
bps,
max_volume_bps,
} => {
map.serialize_entry("surplusBps", &bps)?;
map.serialize_entry("maxVolumeBps", &max_volume_bps)?;
}
FeePolicy::PriceImprovement {
bps,
max_volume_bps,
} => {
map.serialize_entry("priceImprovementBps", &bps)?;
map.serialize_entry("maxVolumeBps", &max_volume_bps)?;
}
}
map.serialize_entry("recipient", &self.recipient)?;
map.end()
}
}
impl<'de> Deserialize<'de> for AppDataPartnerFee {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Helper {
recipient: Address,
#[serde(default)]
bps: Option<u16>,
#[serde(default)]
volume_bps: Option<u16>,
#[serde(default)]
surplus_bps: Option<u16>,
#[serde(default)]
price_improvement_bps: Option<u16>,
#[serde(default)]
max_volume_bps: Option<u16>,
}
let h = Helper::deserialize(deserializer)?;
let policy = match (
h.bps,
h.volume_bps,
h.surplus_bps,
h.price_improvement_bps,
h.max_volume_bps,
) {
(Some(bps), None, None, None, None) | (None, Some(bps), None, None, None) => {
FeePolicy::Volume { bps }
}
(None, None, Some(bps), None, Some(max_volume_bps)) => FeePolicy::Surplus {
bps,
max_volume_bps,
},
(None, None, None, Some(bps), Some(max_volume_bps)) => FeePolicy::PriceImprovement {
bps,
max_volume_bps,
},
_ => {
return Err(D::Error::custom("unknown partner-fee policy shape"));
}
};
Self::new(policy, h.recipient).map_err(D::Error::custom)
}
}
fn validate_fee_policy(policy: &FeePolicy) -> Result<(), AppDataError> {
let check = |field: &'static str, value: u16| -> Result<(), AppDataError> {
if value > PARTNER_FEE_BPS_MAX {
Err(AppDataError::FeeOutOfRange {
field,
value,
max: PARTNER_FEE_BPS_MAX,
})
} else {
Ok(())
}
};
match *policy {
FeePolicy::Volume { bps } => check("bps", bps),
FeePolicy::Surplus {
bps,
max_volume_bps,
} => {
check("surplusBps", bps)?;
check("maxVolumeBps", max_volume_bps)
}
FeePolicy::PriceImprovement {
bps,
max_volume_bps,
} => {
check("priceImprovementBps", bps)?;
check("maxVolumeBps", max_volume_bps)
}
}
}
impl AppDataPartnerFee {
pub fn new(policy: FeePolicy, recipient: Address) -> Result<Self, AppDataError> {
validate_fee_policy(&policy)?;
Ok(Self { policy, recipient })
}
pub const fn policy(&self) -> FeePolicy {
self.policy
}
pub const fn recipient(&self) -> Address {
self.recipient
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FeePolicy {
Volume {
bps: u16,
},
Surplus {
bps: u16,
max_volume_bps: u16,
},
PriceImprovement {
bps: u16,
max_volume_bps: u16,
},
}
#[serde_as]
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppDataFlashloan {
pub liquidity_provider: Address,
pub protocol_adapter: Address,
pub receiver: Address,
pub token: Address,
#[serde_as(as = "DisplayFromStr")]
pub amount: U256,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct AppDataReplacedOrder {
pub uid: OrderUid,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppDataWrapperCall {
pub address: Address,
pub data: Bytes,
#[serde(default)]
pub is_omittable: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppDataReferrer {
pub address: Address,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppDataUtm {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utm_source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utm_medium: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utm_campaign: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utm_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utm_term: Option<String>,
}
pub type AppDataHooks = serde_json::Value;
impl AppDataDoc {
pub fn new(app_code: impl Into<String>) -> Self {
Self {
version: LATEST_APP_DATA_VERSION.to_string(),
app_code: Some(app_code.into()),
environment: None,
metadata: AppDataMetadata::default(),
}
}
pub fn sdk_attribution(app_code: &str) -> Self {
Self {
version: LATEST_APP_DATA_VERSION.to_string(),
app_code: Some(app_code.to_string()),
environment: None,
metadata: AppDataMetadata {
quote: Some(AppDataQuote {
slippage_bips: None,
version: Some(env!("CARGO_PKG_VERSION").to_string()),
}),
..AppDataMetadata::default()
},
}
}
pub fn with_referrer(mut self, address: Address) -> Self {
self.metadata.referrer = Some(AppDataReferrer {
address,
version: None,
});
self
}
pub fn with_partner_fee(self, bps: u16, recipient: Address) -> Result<Self, AppDataError> {
self.with_partner_fee_policy(FeePolicy::Volume { bps }, recipient)
}
pub fn with_partner_fee_policy(
mut self,
policy: FeePolicy,
recipient: Address,
) -> Result<Self, AppDataError> {
self.metadata.partner_fee = Some(AppDataPartnerFee::new(policy, recipient)?);
Ok(self)
}
pub const fn with_flashloan(mut self, flashloan: AppDataFlashloan) -> Self {
self.metadata.flashloan = Some(flashloan);
self
}
pub const fn with_replaced_order(mut self, uid: OrderUid) -> Self {
self.metadata.replaced_order = Some(AppDataReplacedOrder { uid });
self
}
pub fn with_wrapper(mut self, wrapper: AppDataWrapperCall) -> Self {
self.metadata.wrappers.push(wrapper);
self
}
pub const fn with_order_class(mut self, order_class: OrderClass) -> Self {
self.metadata.order_class = Some(AppDataOrderClass { order_class });
self
}
pub fn with_slippage_bips(mut self, slippage_bips: u32) -> Self {
self.metadata
.quote
.get_or_insert_with(AppDataQuote::default)
.slippage_bips = Some(slippage_bips);
self
}
pub fn with_environment(mut self, environment: impl Into<String>) -> Self {
self.environment = Some(environment.into());
self
}
pub fn canonical_json(&self) -> String {
let value = serde_json::to_value(self).expect("AppDataDoc must serialise");
serde_json::to_string(&value).expect("Value must re-serialise")
}
pub fn hash(&self) -> AppDataHash {
self.try_hash()
.expect("AppDataDoc must fit within APP_DATA_SIZE_LIMIT")
}
pub fn try_hash(&self) -> Result<AppDataHash, AppDataError> {
let json = self.canonical_json();
if json.len() > APP_DATA_SIZE_LIMIT {
return Err(AppDataError::DocumentTooLarge {
len: json.len(),
max: APP_DATA_SIZE_LIMIT,
});
}
Ok(keccak256(json.as_bytes()))
}
}
impl std::str::FromStr for AppDataDoc {
type Err = AppDataError;
fn from_str(json: &str) -> Result<Self, Self::Err> {
if json.len() > APP_DATA_SIZE_LIMIT {
return Err(AppDataError::DocumentTooLarge {
len: json.len(),
max: APP_DATA_SIZE_LIMIT,
});
}
serde_json::from_str(json).map_err(|e| AppDataError::Parse(e.to_string()))
}
}
#[derive(Debug, thiserror::Error, Eq, PartialEq)]
pub enum AppDataError {
#[error("app-data document too large: {len} bytes (max {max})")]
DocumentTooLarge {
len: usize,
max: usize,
},
#[error("partner fee {field} = {value} exceeds maximum {max}")]
FeeOutOfRange {
field: &'static str,
value: u16,
max: u16,
},
#[error("invalid app-data JSON: {0}")]
Parse(String),
}
pub const PARTNER_FEE_BPS_MAX: u16 = 10_000;
mod cid;
pub use self::cid::{
AppDataCid, AppDataCidError, MAX_CID_STR_LEN, app_data_cid, app_data_hash_from_cid,
parse_app_data_cid,
};
#[cfg(test)]
pub(crate) use self::cid::{CID_CODEC_RAW, MULTIHASH_KECCAK_256};
#[cfg(test)]
#[path = "app_data/tests.rs"]
mod tests;