use alloy::{
primitives::{Bytes, U256},
sol_types::SolValue,
};
use fastnum::UD64;
use thiserror::Error;
use crate::num;
pub const ORDER_EXTENSION_VERSION: u16 = 1;
pub const MAX_ORDER_EXTENSION_BYTES: usize = 256;
pub const MAX_BUILDER_FEE_PER_100K: u32 = 10_000;
#[derive(Clone, Copy, PartialEq, Eq, derive_more::Debug)]
pub struct BuilderAttribution {
builder_id: super::BuilderId,
#[debug("{fee}")]
fee: UD64,
}
#[derive(Clone, Debug, Error)]
pub enum OrderExtensionError {
#[error("order extension of {0} bytes exceeds maximum of {MAX_ORDER_EXTENSION_BYTES}")]
ExceedsMaximumSize(usize),
#[error("malformed order extension envelope")]
Malformed,
#[error("unsupported order extension version: {0}")]
UnsupportedVersion(u16),
#[error("builder id {id} exceeds maximum of 255")]
BuilderIdExceedsMaximum { id: U256 },
#[error("builder fee {0} exceeds maximum of {MAX_BUILDER_FEE_PER_100K} Per100K")]
FeeExceedsMaximum(U256),
}
impl BuilderAttribution {
pub fn new(builder_id: super::BuilderId, fee: UD64) -> Self { Self { builder_id, fee } }
pub(crate) fn from_raw(builder_id: super::BuilderId, fee_per_100k: U256) -> Self {
Self { builder_id, fee: num::fee_converter().from_unsigned(fee_per_100k) }
}
pub fn builder_id(&self) -> super::BuilderId { self.builder_id }
pub fn fee(&self) -> UD64 { self.fee }
pub fn fee_per_100k(&self) -> U256 { num::fee_converter().to_unsigned(self.fee) }
pub fn encode(&self) -> Result<Bytes, OrderExtensionError> {
let fee_per_100k = self.fee_per_100k();
if fee_per_100k > U256::from(MAX_BUILDER_FEE_PER_100K) {
return Err(OrderExtensionError::FeeExceedsMaximum(fee_per_100k));
}
let payload = (U256::from(self.builder_id), fee_per_100k).abi_encode_params();
Ok((ORDER_EXTENSION_VERSION, Bytes::from(payload))
.abi_encode_params()
.into())
}
pub fn decode(extension: &[u8]) -> Result<Option<Self>, OrderExtensionError> {
if extension.is_empty() {
return Ok(None);
}
if extension.len() > MAX_ORDER_EXTENSION_BYTES {
return Err(OrderExtensionError::ExceedsMaximumSize(extension.len()));
}
let (version, payload) = <(u16, Bytes)>::abi_decode_params(extension)
.map_err(|_| OrderExtensionError::Malformed)?;
if version != ORDER_EXTENSION_VERSION {
return Err(OrderExtensionError::UnsupportedVersion(version));
}
let (builder_id, fee_per_100k) = <(U256, U256)>::abi_decode_params(&payload)
.map_err(|_| OrderExtensionError::Malformed)?;
if builder_id > U256::from(u8::MAX) {
return Err(OrderExtensionError::BuilderIdExceedsMaximum { id: builder_id });
}
if fee_per_100k > U256::from(MAX_BUILDER_FEE_PER_100K) {
return Err(OrderExtensionError::FeeExceedsMaximum(fee_per_100k));
}
Ok(Some(Self::from_raw(builder_id.to(), fee_per_100k)))
}
}
impl std::fmt::Display for BuilderAttribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "🏗{}@{}", self.builder_id, self.fee)
}
}
#[cfg(test)]
mod tests {
use fastnum::udec64;
use super::*;
#[test]
fn builder_attribution_envelope_round_trip() {
let attribution = BuilderAttribution::new(7, udec64!(0.001));
let encoded = attribution.encode().expect("fee within range");
assert_eq!(encoded.len(), 160);
assert!(encoded.len() <= MAX_ORDER_EXTENSION_BYTES);
assert_eq!(attribution.fee_per_100k(), U256::from(100));
assert_eq!(BuilderAttribution::decode(&encoded).unwrap(), Some(attribution));
}
#[test]
fn empty_envelope_is_no_builder() {
assert_eq!(BuilderAttribution::decode(&[]).unwrap(), None);
}
#[test]
fn rejects_out_of_range_fee() {
let attribution = BuilderAttribution::new(1, udec64!(0.11));
assert!(matches!(attribution.encode(), Err(OrderExtensionError::FeeExceedsMaximum(_)),));
let payload = (U256::from(1), U256::from(11_000)).abi_encode_params();
let envelope: Bytes = (ORDER_EXTENSION_VERSION, Bytes::from(payload))
.abi_encode_params()
.into();
assert!(matches!(
BuilderAttribution::decode(&envelope),
Err(OrderExtensionError::FeeExceedsMaximum(_)),
));
}
#[test]
fn rejects_unknown_version_and_malformed_envelope() {
let payload = (U256::from(1), U256::from(10)).abi_encode_params();
let envelope: Bytes = (2u16, Bytes::from(payload)).abi_encode_params().into();
assert!(matches!(
BuilderAttribution::decode(&envelope),
Err(OrderExtensionError::UnsupportedVersion(2)),
));
assert!(matches!(
BuilderAttribution::decode(&[0xffu8; 3]),
Err(OrderExtensionError::Malformed),
));
assert!(matches!(
BuilderAttribution::decode(&[0u8; MAX_ORDER_EXTENSION_BYTES + 1]),
Err(OrderExtensionError::ExceedsMaximumSize(_)),
));
}
}