perpl_sdk/types/
extension.rs1use alloy::{
4 primitives::{Bytes, U256},
5 sol_types::SolValue,
6};
7use fastnum::UD64;
8use thiserror::Error;
9
10use crate::num;
11
12pub const ORDER_EXTENSION_VERSION: u16 = 1;
15
16pub const MAX_ORDER_EXTENSION_BYTES: usize = 256;
22
23pub const MAX_BUILDER_FEE_PER_100K: u32 = 10_000;
26
27#[derive(Clone, Copy, PartialEq, Eq, derive_more::Debug)]
37pub struct BuilderAttribution {
38 builder_id: super::BuilderId,
39 #[debug("{fee}")]
40 fee: UD64,
41}
42
43#[derive(Clone, Debug, Error)]
45pub enum OrderExtensionError {
46 #[error("order extension of {0} bytes exceeds maximum of {MAX_ORDER_EXTENSION_BYTES}")]
49 ExceedsMaximumSize(usize),
50
51 #[error("malformed order extension envelope")]
55 Malformed,
56
57 #[error("unsupported order extension version: {0}")]
61 UnsupportedVersion(u16),
62
63 #[error("builder id {id} exceeds maximum of 255")]
65 BuilderIdExceedsMaximum { id: U256 },
66
67 #[error("builder fee {0} exceeds maximum of {MAX_BUILDER_FEE_PER_100K} Per100K")]
70 FeeExceedsMaximum(U256),
71}
72
73impl BuilderAttribution {
74 pub fn new(builder_id: super::BuilderId, fee: UD64) -> Self { Self { builder_id, fee } }
81
82 pub(crate) fn from_raw(builder_id: super::BuilderId, fee_per_100k: U256) -> Self {
85 Self { builder_id, fee: num::fee_converter().from_unsigned(fee_per_100k) }
86 }
87
88 pub fn builder_id(&self) -> super::BuilderId { self.builder_id }
91
92 pub fn fee(&self) -> UD64 { self.fee }
95
96 pub fn fee_per_100k(&self) -> U256 { num::fee_converter().to_unsigned(self.fee) }
98
99 pub fn encode(&self) -> Result<Bytes, OrderExtensionError> {
111 let fee_per_100k = self.fee_per_100k();
112 if fee_per_100k > U256::from(MAX_BUILDER_FEE_PER_100K) {
113 return Err(OrderExtensionError::FeeExceedsMaximum(fee_per_100k));
114 }
115 let payload = (U256::from(self.builder_id), fee_per_100k).abi_encode_params();
116 Ok((ORDER_EXTENSION_VERSION, Bytes::from(payload))
117 .abi_encode_params()
118 .into())
119 }
120
121 pub fn decode(extension: &[u8]) -> Result<Option<Self>, OrderExtensionError> {
129 if extension.is_empty() {
130 return Ok(None);
131 }
132 if extension.len() > MAX_ORDER_EXTENSION_BYTES {
133 return Err(OrderExtensionError::ExceedsMaximumSize(extension.len()));
134 }
135 let (version, payload) = <(u16, Bytes)>::abi_decode_params(extension)
136 .map_err(|_| OrderExtensionError::Malformed)?;
137 if version != ORDER_EXTENSION_VERSION {
138 return Err(OrderExtensionError::UnsupportedVersion(version));
139 }
140 let (builder_id, fee_per_100k) = <(U256, U256)>::abi_decode_params(&payload)
141 .map_err(|_| OrderExtensionError::Malformed)?;
142 if builder_id > U256::from(u8::MAX) {
143 return Err(OrderExtensionError::BuilderIdExceedsMaximum { id: builder_id });
144 }
145 if fee_per_100k > U256::from(MAX_BUILDER_FEE_PER_100K) {
146 return Err(OrderExtensionError::FeeExceedsMaximum(fee_per_100k));
147 }
148 Ok(Some(Self::from_raw(builder_id.to(), fee_per_100k)))
149 }
150}
151
152impl std::fmt::Display for BuilderAttribution {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(f, "🏗{}@{}", self.builder_id, self.fee)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use fastnum::udec64;
161
162 use super::*;
163
164 #[test]
165 fn builder_attribution_envelope_round_trip() {
166 let attribution = BuilderAttribution::new(7, udec64!(0.001));
167 let encoded = attribution.encode().expect("fee within range");
168
169 assert_eq!(encoded.len(), 160);
172 assert!(encoded.len() <= MAX_ORDER_EXTENSION_BYTES);
173 assert_eq!(attribution.fee_per_100k(), U256::from(100));
174 assert_eq!(BuilderAttribution::decode(&encoded).unwrap(), Some(attribution));
175 }
176
177 #[test]
178 fn empty_envelope_is_no_builder() {
179 assert_eq!(BuilderAttribution::decode(&[]).unwrap(), None);
180 }
181
182 #[test]
183 fn rejects_out_of_range_fee() {
184 let attribution = BuilderAttribution::new(1, udec64!(0.11));
186 assert!(matches!(attribution.encode(), Err(OrderExtensionError::FeeExceedsMaximum(_)),));
187
188 let payload = (U256::from(1), U256::from(11_000)).abi_encode_params();
192 let envelope: Bytes = (ORDER_EXTENSION_VERSION, Bytes::from(payload))
193 .abi_encode_params()
194 .into();
195 assert!(matches!(
196 BuilderAttribution::decode(&envelope),
197 Err(OrderExtensionError::FeeExceedsMaximum(_)),
198 ));
199 }
200
201 #[test]
202 fn rejects_unknown_version_and_malformed_envelope() {
203 let payload = (U256::from(1), U256::from(10)).abi_encode_params();
204 let envelope: Bytes = (2u16, Bytes::from(payload)).abi_encode_params().into();
205 assert!(matches!(
206 BuilderAttribution::decode(&envelope),
207 Err(OrderExtensionError::UnsupportedVersion(2)),
208 ));
209
210 assert!(matches!(
211 BuilderAttribution::decode(&[0xffu8; 3]),
212 Err(OrderExtensionError::Malformed),
213 ));
214 assert!(matches!(
215 BuilderAttribution::decode(&[0u8; MAX_ORDER_EXTENSION_BYTES + 1]),
216 Err(OrderExtensionError::ExceedsMaximumSize(_)),
217 ));
218 }
219}