@@ -4,23 +4,27 @@ use alloy::primitives::U256;
44use tycho_client:: feed:: { synchronizer:: ComponentWithState , BlockHeader } ;
55use tycho_common:: { models:: token:: Token , Bytes } ;
66
7- use super :: { enums :: FeeAmount , state:: UniswapV3State } ;
7+ use super :: state:: AerodromeSlipstreamsState ;
88use crate :: {
9- evm:: protocol:: utils:: uniswap:: { i24_be_bytes_to_i32, tick_list:: TickInfo } ,
9+ evm:: protocol:: utils:: {
10+ slipstreams:: { dynamic_fee_module:: DynamicFeeConfig , observations:: Observation } ,
11+ uniswap:: { i24_be_bytes_to_i32, tick_list:: TickInfo } ,
12+ } ,
1013 protocol:: {
1114 errors:: InvalidSnapshotError ,
1215 models:: { DecoderContext , TryFromWithBlock } ,
1316 } ,
1417} ;
1518
16- impl TryFromWithBlock < ComponentWithState , BlockHeader > for UniswapV3State {
19+ impl TryFromWithBlock < ComponentWithState , BlockHeader > for AerodromeSlipstreamsState {
1720 type Error = InvalidSnapshotError ;
1821
19- /// Decodes a `ComponentWithState` into a `UniswapV3State`. Errors with a `InvalidSnapshotError`
20- /// if the snapshot is missing any required attributes or if the fee amount is not supported.
22+ /// Decodes a `ComponentWithState` into a `AerodromeSlipstreamsState`. Errors with a
23+ /// `InvalidSnapshotError` if the snapshot is missing any required attributes or if the fee
24+ /// amount is not supported.
2125 async fn try_from_with_header (
2226 snapshot : ComponentWithState ,
23- _block : BlockHeader ,
27+ block : BlockHeader ,
2428 _account_balances : & HashMap < Bytes , HashMap < Bytes , Bytes > > ,
2529 _all_tokens : & HashMap < Bytes , Token > ,
2630 _decoder_context : & DecoderContext ,
@@ -58,16 +62,87 @@ impl TryFromWithBlock<ComponentWithState, BlockHeader> for UniswapV3State {
5862 . ok_or_else ( || InvalidSnapshotError :: MissingAttribute ( "sqrt_price" . to_string ( ) ) ) ?,
5963 ) ;
6064
61- let fee_value = i32:: from (
65+ let observation_index = u16:: from (
66+ snapshot
67+ . component
68+ . static_attributes
69+ . get ( "observationIndex" )
70+ . ok_or_else ( || {
71+ InvalidSnapshotError :: MissingAttribute ( "observationIndex" . to_string ( ) )
72+ } ) ?
73+ . clone ( ) ,
74+ ) ;
75+
76+ let observation_cardinality = u16:: from (
77+ snapshot
78+ . state
79+ . attributes
80+ . get ( "observationCardinality" )
81+ . ok_or_else ( || {
82+ InvalidSnapshotError :: MissingAttribute ( "observationCardinality" . to_string ( ) )
83+ } ) ?
84+ . clone ( ) ,
85+ ) ;
86+
87+ let dfc_base_fee = u32:: from (
88+ snapshot
89+ . component
90+ . static_attributes
91+ . get ( "dfc_baseFee" )
92+ . ok_or_else ( || InvalidSnapshotError :: MissingAttribute ( "dfc_baseFee" . to_string ( ) ) ) ?
93+ . clone ( ) ,
94+ ) ;
95+
96+ let dfc_scaling_factor = u64:: from (
97+ snapshot
98+ . state
99+ . attributes
100+ . get ( "dfc_scalingFactor" )
101+ . ok_or_else ( || {
102+ InvalidSnapshotError :: MissingAttribute ( "dfc_scalingFactor" . to_string ( ) )
103+ } ) ?
104+ . clone ( ) ,
105+ ) ;
106+
107+ let dfc_fee_cap = u32:: from (
108+ snapshot
109+ . component
110+ . static_attributes
111+ . get ( "dfc_feeCap" )
112+ . ok_or_else ( || InvalidSnapshotError :: MissingAttribute ( "dfc_feeCap" . to_string ( ) ) ) ?
113+ . clone ( ) ,
114+ ) ;
115+
116+ let tick_spacing = snapshot
117+ . component
118+ . static_attributes
119+ . get ( "tick_spacing" )
120+ . ok_or_else ( || InvalidSnapshotError :: MissingAttribute ( "tick_spacing" . to_string ( ) ) ) ?
121+ . clone ( ) ;
122+
123+ let tick_spacing_4_bytes = if tick_spacing. len ( ) == 32 {
124+ // Make sure it only happens for 0 values, otherwise error.
125+ if tick_spacing == Bytes :: zero ( 32 ) {
126+ Bytes :: from ( [ 0 ; 4 ] )
127+ } else {
128+ return Err ( InvalidSnapshotError :: ValueError ( format ! (
129+ "Tick Spacing bytes too long for {tick_spacing}, expected 4"
130+ ) ) ) ;
131+ }
132+ } else {
133+ tick_spacing
134+ } ;
135+
136+ let tick_spacing = i24_be_bytes_to_i32 ( & tick_spacing_4_bytes) ;
137+
138+ let default_fee = u32:: from (
62139 snapshot
63140 . component
64141 . static_attributes
65- . get ( "fee " )
66- . ok_or_else ( || InvalidSnapshotError :: MissingAttribute ( "fee " . to_string ( ) ) ) ?
142+ . get ( "default_fee " )
143+ . ok_or_else ( || InvalidSnapshotError :: MissingAttribute ( "default_fee " . to_string ( ) ) ) ?
67144 . clone ( ) ,
68145 ) ;
69- let fee = FeeAmount :: try_from ( fee_value)
70- . map_err ( |_| InvalidSnapshotError :: ValueError ( "Unsupported fee amount" . to_string ( ) ) ) ?;
71146
72147 let tick = snapshot
73148 . state
@@ -126,185 +201,53 @@ impl TryFromWithBlock<ComponentWithState, BlockHeader> for UniswapV3State {
126201
127202 ticks. sort_by_key ( |tick| tick. index ) ;
128203
129- UniswapV3State :: new ( liquidity, sqrt_price, fee, tick, ticks)
130- . map_err ( |err| InvalidSnapshotError :: ValueError ( err. to_string ( ) ) )
131- }
132- }
133-
134- #[ cfg( test) ]
135- mod tests {
136- use std:: str:: FromStr ;
137-
138- use chrono:: DateTime ;
139- use rstest:: rstest;
140- use tycho_common:: dto:: { Chain , ChangeType , ProtocolComponent , ResponseProtocolState } ;
141-
142- use super :: * ;
143-
144- fn usv3_component ( ) -> ProtocolComponent {
145- let creation_time = DateTime :: from_timestamp ( 1622526000 , 0 )
146- . unwrap ( )
147- . naive_utc ( ) ; //Sample timestamp
148-
149- // Add a static attribute "fee"
150- let mut static_attributes: HashMap < String , Bytes > = HashMap :: new ( ) ;
151- static_attributes. insert ( "fee" . to_string ( ) , Bytes :: from ( 3000_i32 . to_be_bytes ( ) . to_vec ( ) ) ) ;
152-
153- ProtocolComponent {
154- id : "State1" . to_string ( ) ,
155- protocol_system : "system1" . to_string ( ) ,
156- protocol_type_name : "typename1" . to_string ( ) ,
157- chain : Chain :: Ethereum ,
158- tokens : Vec :: new ( ) ,
159- contract_ids : Vec :: new ( ) ,
160- static_attributes,
161- change : ChangeType :: Creation ,
162- creation_tx : Bytes :: from_str ( "0x0000" ) . unwrap ( ) ,
163- created_at : creation_time,
164- }
165- }
166-
167- fn usv3_attributes ( ) -> HashMap < String , Bytes > {
168- vec ! [
169- ( "liquidity" . to_string( ) , Bytes :: from( 100_u64 . to_be_bytes( ) . to_vec( ) ) ) ,
170- ( "sqrt_price_x96" . to_string( ) , Bytes :: from( 200_u64 . to_be_bytes( ) . to_vec( ) ) ) ,
171- ( "tick" . to_string( ) , Bytes :: from( 300_i32 . to_be_bytes( ) . to_vec( ) ) ) ,
172- ( "ticks/60/net_liquidity" . to_string( ) , Bytes :: from( 400_i128 . to_be_bytes( ) . to_vec( ) ) ) ,
173- ]
174- . into_iter ( )
175- . collect :: < HashMap < String , Bytes > > ( )
176- }
177-
178- fn header ( ) -> BlockHeader {
179- BlockHeader {
180- number : 1 ,
181- hash : Bytes :: from ( vec ! [ 0 ; 32 ] ) ,
182- parent_hash : Bytes :: from ( vec ! [ 0 ; 32 ] ) ,
183- revert : false ,
184- timestamp : 1 ,
185- }
186- }
187-
188- #[ tokio:: test]
189- async fn test_usv3_try_from ( ) {
190- let snapshot = ComponentWithState {
191- state : ResponseProtocolState {
192- component_id : "State1" . to_owned ( ) ,
193- attributes : usv3_attributes ( ) ,
194- balances : HashMap :: new ( ) ,
195- } ,
196- component : usv3_component ( ) ,
197- component_tvl : None ,
198- entrypoints : Vec :: new ( ) ,
199- } ;
200-
201- let result = UniswapV3State :: try_from_with_header (
202- snapshot,
203- header ( ) ,
204- & HashMap :: new ( ) ,
205- & HashMap :: new ( ) ,
206- & DecoderContext :: new ( ) ,
207- )
208- . await ;
209-
210- assert ! ( result. is_ok( ) ) ;
211- let expected = UniswapV3State :: new (
212- 100 ,
213- U256 :: from ( 200 ) ,
214- FeeAmount :: Medium ,
215- 300 ,
216- vec ! [ TickInfo :: new( 60 , 400 ) . unwrap( ) ] ,
217- )
218- . unwrap ( ) ;
219- assert_eq ! ( result. unwrap( ) , expected) ;
220- }
221-
222- #[ tokio:: test]
223- #[ rstest]
224- #[ case:: missing_liquidity( "liquidity" ) ]
225- #[ case:: missing_sqrt_price( "sqrt_price" ) ]
226- #[ case:: missing_tick( "tick" ) ]
227- #[ case:: missing_tick_liquidity( "tick_liquidities" ) ]
228- #[ case:: missing_fee( "fee" ) ]
229- async fn test_usv3_try_from_invalid ( #[ case] missing_attribute : String ) {
230- // remove missing attribute
231- let mut attributes = usv3_attributes ( ) ;
232- attributes. remove ( & missing_attribute) ;
233-
234- if missing_attribute == "tick_liquidities" {
235- attributes. remove ( "ticks/60/net_liquidity" ) ;
236- }
237-
238- if missing_attribute == "sqrt_price" {
239- attributes. remove ( "sqrt_price_x96" ) ;
240- }
241-
242- let mut component = usv3_component ( ) ;
243- if missing_attribute == "fee" {
244- component
245- . static_attributes
246- . remove ( "fee" ) ;
247- }
248-
249- let snapshot = ComponentWithState {
250- state : ResponseProtocolState {
251- component_id : "State1" . to_owned ( ) ,
252- attributes,
253- balances : HashMap :: new ( ) ,
254- } ,
255- component,
256- component_tvl : None ,
257- entrypoints : Vec :: new ( ) ,
258- } ;
259-
260- let result = UniswapV3State :: try_from_with_header (
261- snapshot,
262- header ( ) ,
263- & HashMap :: new ( ) ,
264- & HashMap :: new ( ) ,
265- & DecoderContext :: new ( ) ,
266- )
267- . await ;
268-
269- assert ! ( result. is_err( ) ) ;
270- assert ! ( matches!(
271- result. err( ) . unwrap( ) ,
272- InvalidSnapshotError :: MissingAttribute ( attr) if attr == missing_attribute
273- ) ) ;
274- }
275-
276- #[ tokio:: test]
277- async fn test_usv3_try_from_invalid_fee ( ) {
278- // set an invalid fee amount (100, 500, 3_000 and 10_000 are the only valid fee amounts)
279- let mut component = usv3_component ( ) ;
280- component
281- . static_attributes
282- . insert ( "fee" . to_string ( ) , Bytes :: from ( 4000_i32 . to_be_bytes ( ) . to_vec ( ) ) ) ;
204+ let observations: Result < Vec < _ > , _ > = snapshot
205+ . state
206+ . attributes
207+ . iter ( )
208+ . filter_map ( |( key, value) | {
209+ if key. starts_with ( "observations/" ) {
210+ Some (
211+ key. split ( '/' )
212+ . nth ( 1 ) ?
213+ . parse :: < i32 > ( )
214+ . map_err ( |err| InvalidSnapshotError :: ValueError ( err. to_string ( ) ) )
215+ . and_then ( |observation_idx| {
216+ Observation :: from_attribute ( observation_idx, & value. clone ( ) )
217+ . map_err ( |err| {
218+ InvalidSnapshotError :: ValueError ( err. to_string ( ) )
219+ } )
220+ } ) ,
221+ )
222+ } else {
223+ None
224+ }
225+ } )
226+ . collect ( ) ;
283227
284- let snapshot = ComponentWithState {
285- state : ResponseProtocolState {
286- component_id : "State1" . to_owned ( ) ,
287- attributes : usv3_attributes ( ) ,
288- balances : HashMap :: new ( ) ,
289- } ,
290- component,
291- component_tvl : None ,
292- entrypoints : Vec :: new ( ) ,
228+ let mut observations = match observations {
229+ Ok ( observations) if !observations. is_empty ( ) => observations
230+ . into_iter ( )
231+ . filter ( |t| t. initialized )
232+ . collect :: < Vec < _ > > ( ) ,
233+ _ => return Err ( InvalidSnapshotError :: MissingAttribute ( "observations" . to_string ( ) ) ) ,
293234 } ;
294235
295- let result = UniswapV3State :: try_from_with_header (
296- snapshot,
297- header ( ) ,
298- & HashMap :: new ( ) ,
299- & HashMap :: new ( ) ,
300- & DecoderContext :: new ( ) ,
236+ observations. sort_by_key ( |observation| observation. index ) ;
237+
238+ AerodromeSlipstreamsState :: new (
239+ block. timestamp ,
240+ liquidity,
241+ sqrt_price,
242+ observation_index,
243+ observation_cardinality,
244+ default_fee,
245+ tick,
246+ tick_spacing,
247+ ticks,
248+ observations,
249+ DynamicFeeConfig :: new ( dfc_base_fee, dfc_fee_cap, dfc_scaling_factor) ,
301250 )
302- . await ;
303-
304- assert ! ( result. is_err( ) ) ;
305- assert ! ( matches!(
306- result. err( ) . unwrap( ) ,
307- InvalidSnapshotError :: ValueError ( err) if err == * "Unsupported fee amount"
308- ) ) ;
251+ . map_err ( |err| InvalidSnapshotError :: ValueError ( err. to_string ( ) ) )
309252 }
310253}
0 commit comments