feat: add LunarBase protocol integration#1053
Conversation
| let component = to_tycho_protocol_component(lunarbase::protocol_component( | ||
| config.pool, | ||
| config.token_x, | ||
| config.token_y, | ||
| )); |
There was a problem hiding this comment.
Does LunarBase only have one pool for now? Any plans to add more later? If new pools are added, can we discover them through events?
There was a problem hiding this comment.
added support for multiple explicitly configured pools via the pools=... parameter, so each configured pool becomes a separate Tycho component. If an on-chain factory is added later, discovery can be moved from config to factory events.
| #[substreams::handlers::store] | ||
| pub fn store_protocol_components( | ||
| components: tycho::BlockTransactionProtocolComponents, | ||
| store: StoreSetIfNotExistsProto<tycho::ProtocolComponent>, | ||
| ) { | ||
| for tx_components in components.tx_components { | ||
| for component in tx_components.components { | ||
| store.set_if_not_exists(0, component_key(&component.id), &component); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This store isn’t used right now. I saw that map_protocol_changes checks whether the pair exists, I think it should read from this store for that check.
There was a problem hiding this comment.
map_protocol_changes now reads from store_protocol_components when checking known components.
| let component_is_known = config | ||
| .bootstrap_block | ||
| .map(|bootstrap_block| block.number >= bootstrap_block) | ||
| .unwrap_or(true) || | ||
| new_components | ||
| .tx_components | ||
| .iter() | ||
| .flat_map(|tx| tx.components.iter()) | ||
| .any(|component| component.id == component_id); | ||
|
|
||
| let known_components = component_is_known | ||
| .then(|| component_id.clone()) | ||
| .into_iter() | ||
| .collect::<Vec<_>>(); |
There was a problem hiding this comment.
Do you mean there could be multiple components here?
| .with_attributes( | ||
| &component | ||
| .static_attributes | ||
| .into_iter() | ||
| .sorted_unstable_by(|(left, _), (right, _)| left.cmp(right)) | ||
| .collect::<Vec<_>>(), | ||
| ) |
There was a problem hiding this comment.
Why do we need to add the static_attribute into attributes? In Tycho, static_attributes are immutable, while attributes are mutable, so it’s better to keep them separate.
There was a problem hiding this comment.
removed with_attributes(...) call here and stopped duplicating pool/token data into component static attributes
| pub mod topics { | ||
| pub const SWAP_EXECUTED: &str = | ||
| "0x1b43ddf90e971181a7faf41549e512675072e84befadbba7873086509dec1fdc"; | ||
| pub const STATE_UPDATED: &str = | ||
| "0x8acb811d2c5106785f847faf03ce160d2eb124b8632eb42d466f46c087033d61"; | ||
| pub const BLOCK_DELAY_SET: &str = | ||
| "0x673f9280467ef1d677edd6a21630cf328068a1dc8da64205c1bc79855c6b2307"; | ||
| pub const CONCENTRATION_K_SET: &str = | ||
| "0xcf34ec77e4a73dc1b2fdbb6eaec360819374b6412a8bf8096f91c4fdb76db3a8"; | ||
| pub const WHITELIST_SET: &str = | ||
| "0x0aa5ec5ffdc7f6f9c4d0dded489d7450297155cb2f71cb771e02427f7dff4f51"; | ||
| pub const BLACKLIST_FEE_MULTIPLIER_SET: &str = | ||
| "0xa15057886e6ebcdf47294bcb091d686031124d1041cafe00740e93667bacd186"; | ||
| pub const SYNC: &str = "0x99e93fd94a51b80d7dd7ec3f69c4f09a43e7523f5a45ca09b88a178d9daaed1e"; | ||
| pub const PAUSED: &str = "0x62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a258"; | ||
| pub const UNPAUSED: &str = "0x5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa"; | ||
| } |
There was a problem hiding this comment.
Can we use the ABI-generated Rust code to distinguish the events instead?
There was a problem hiding this comment.
now using ABI-generated bindings instead of manual topic constants
| fn merge_state_delta(tx: &mut TransactionChanges, component_id: &str, delta: StateDelta) { | ||
| let entry = tx | ||
| .state_updates | ||
| .entry(component_id.to_owned()) | ||
| .or_default(); | ||
|
|
||
| for deleted in delta.deleted_attributes { | ||
| entry | ||
| .updated_attributes | ||
| .remove(&deleted); | ||
| entry.deleted_attributes.insert(deleted); | ||
| } | ||
|
|
||
| for (name, value) in delta.updated_attributes { | ||
| entry.deleted_attributes.remove(&name); | ||
| entry | ||
| .updated_attributes | ||
| .insert(name, value); | ||
| } | ||
| } |
There was a problem hiding this comment.
I see deleted_attributes is always an empty hashmap, so do we still need this merge here?
There was a problem hiding this comment.
the merge now only applies updated attributes
| pool: require_address(attrs, attrs::POOL)?, | ||
| token_x: require_address(attrs, attrs::TOKEN_X)?, | ||
| token_y: require_address(attrs, attrs::TOKEN_Y)?, |
There was a problem hiding this comment.
These three attributes are static_attributes, so they won’t be readable here.
| anchor_price_x96: require_u128(attrs, attrs::ANCHOR_PRICE_X96)?, | ||
| fee_ask_x24: require_u32(attrs, attrs::FEE_ASK_X24)?, | ||
| fee_bid_x24: require_u32(attrs, attrs::FEE_BID_X24)?, | ||
| latest_update_block: require_u64(attrs, attrs::LATEST_UPDATE_BLOCK)?, | ||
| reserve_x: require_u128(attrs, attrs::RESERVE_X)?, | ||
| reserve_y: require_u128(attrs, attrs::RESERVE_Y)?, | ||
| concentration_k: require_u32(attrs, attrs::CONCENTRATION_K)?, | ||
| block_delay: require_u64(attrs, attrs::BLOCK_DELAY)?, | ||
| paused: require_bool(attrs, attrs::PAUSED)?, | ||
| blacklist_fee_multiplier: require_u256(attrs, attrs::BLACKLIST_FEE_MULTIPLIER)?, | ||
| executor_whitelisted: require_bool(attrs, attrs::EXECUTOR_WHITELISTED)?, |
There was a problem hiding this comment.
These attributes don’t have defaults in the substreams, so they may not exist if the events never fire. Better to set defaults for them in the substreams.
| fn delta_transition( | ||
| &mut self, | ||
| delta: ProtocolStateDelta, | ||
| _tokens: &HashMap<Bytes, Token>, | ||
| _balances: &Balances, | ||
| ) -> Result<(), TransitionError> { | ||
| let state_delta = StateDelta { | ||
| updated_attributes: delta | ||
| .updated_attributes | ||
| .into_iter() | ||
| .map(|(key, value)| (key, value.to_vec())) | ||
| .collect(), | ||
| deleted_attributes: delta | ||
| .deleted_attributes | ||
| .into_iter() | ||
| .collect(), | ||
| }; | ||
| apply_delta(&mut self.state, &state_delta) | ||
| .map_err(|err| TransitionError::DecodeError(format!("{err:?}"))) | ||
| } |
There was a problem hiding this comment.
head_block isn’t being updated.
There was a problem hiding this comment.
delta_transition now reads block_number, filters it out of protocol attrs, and updates head_block
| if sell == self.state.token_x && buy == self.state.token_y { | ||
| return Ok((BigUint::ZERO, BigUint::from(self.state.reserve_x))); | ||
| } | ||
| if sell == self.state.token_y && buy == self.state.token_x { | ||
| return Ok((BigUint::ZERO, BigUint::from(self.state.reserve_y))); | ||
| } |
There was a problem hiding this comment.
get_limits should return the max input amount for the sell_token and the max output amount for the buy_token. Right now, the first value is 0, that doesn’t match the expected definition.
| fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> { | ||
| match params.swap_constraint() { | ||
| SwapConstraint::TradeLimitPrice { .. } | SwapConstraint::PoolTargetPrice { .. } => { | ||
| Err(SimulationError::InvalidInput( | ||
| "LunarBase native simulator only supports exact-input get_amount_out" | ||
| .to_owned(), | ||
| None, | ||
| )) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This function doesn’t seem to be implemented yet.
There was a problem hiding this comment.
now delegates to shared query_pool_swap helper
6069396 to
80e5fd0
Compare
| fn known_pool_component( | ||
| pool: &PoolConfig, | ||
| new_components: &tycho::BlockTransactionProtocolComponents, | ||
| component_store: &StoreGetProto<tycho::ProtocolComponent>, | ||
| ) -> Option<String> { | ||
| let component_id = pool.component_id(); | ||
| let known = pool.bootstrap_block.is_none() || | ||
| component_store | ||
| .get_last(component_key(&component_id)) | ||
| .is_some() || | ||
| new_components | ||
| .tx_components | ||
| .iter() | ||
| .flat_map(|tx| tx.components.iter()) | ||
| .any(|component| component.id == component_id); | ||
| known.then_some(component_id) | ||
| } |
There was a problem hiding this comment.
can we just check component_store? New components should already be written to the store earlier.
| let Some(pool) = pool_by_address(&config, &log.address) else { | ||
| continue; | ||
| }; |
There was a problem hiding this comment.
Should we filter out pools whose components haven’t been created yet here?
| LunarBaseEvent::WhitelistSet { account, whitelisted } => { | ||
| if *account == context.tycho_router { | ||
| insert_bool(&mut updated_attributes, attrs::SWAP_CALLER_WHITELISTED, *whitelisted); | ||
| } | ||
| } |
There was a problem hiding this comment.
I noticed the tycho_router config is still empty. How are you planning to handle the whitelist? Will all pools be whitelisted for tycho_router, or only some?
If all pools are whitelisted, can we remove this attribute? Then In tycho-simulation, pools are whitelisted by default.
There was a problem hiding this comment.
our partners are whitelisted by default, so I will remove this attribute
| #[derive(Clone, Debug, PartialEq, Eq)] | ||
| pub struct BalanceChange { | ||
| pub token: Address, | ||
| pub balance: u128, | ||
| } | ||
|
|
||
| #[derive(Clone, Debug, PartialEq, Eq)] | ||
| pub struct TransactionChanges { | ||
| pub tx: IndexedTransaction, | ||
| pub new_protocol_components: Vec<ProtocolComponent>, | ||
| pub state_updates: HashMap<String, StateDelta>, | ||
| pub balance_changes: HashMap<String, Vec<BalanceChange>>, | ||
| } | ||
|
|
||
| #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | ||
| pub struct IndexedTransaction { | ||
| pub hash: [u8; 32], | ||
| pub from: Address, | ||
| pub to: Address, | ||
| pub index: u64, | ||
| } | ||
|
|
||
| #[derive(Clone, Debug, PartialEq, Eq)] | ||
| pub struct BlockChanges { | ||
| pub transactions: Vec<TransactionChanges>, | ||
| } |
There was a problem hiding this comment.
I’m not really a fan of adding these extra structs. They already exist in tycho-substreams, and if we define new ones, we’ll need extra conversion logic in tycho-mapper too.
ae46904 to
69e6db4
Compare
|
@zach030, pls check latest commit, fixed |
| let quote = quote_exact_in( | ||
| state, | ||
| QuoteRequest { | ||
| token_in, | ||
| token_out, | ||
| amount_in: biguint_to_u256(&amount_in)?, | ||
| block_number: state.latest_update_block, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Here we use latest_update_block, but get_amount_out uses head_block. Could that cause issues?
| pub concentration_k: u32, | ||
| pub block_delay: u64, | ||
| pub paused: bool, | ||
| pub blacklist_fee_multiplier: U256, |
There was a problem hiding this comment.
We don’t use blacklist_fee_multiplier anymore, right? If so, we can remove it and the related logic from both tycho-simulation and the substreams.
| #[derive(Clone, Debug, PartialEq, Eq)] | ||
| pub enum AttributeError { | ||
| Missing(&'static str), | ||
| InvalidLength { name: &'static str, expected: usize, actual: usize }, | ||
| IntegerOverflow(&'static str), | ||
| } |
There was a problem hiding this comment.
no need to define a separate AttributeError. We already have the InvalidSnapshotError enum.
| pub state: LunarBaseState, | ||
| pub head_block: u64, | ||
| } | ||
|
|
There was a problem hiding this comment.
I don’t think we need two state structs here. Just keeping LunarBaseTychoState with all fields should be enough.
| fn insert_bool(attrs: &mut AttributeMap, name: &'static str, value: bool) { | ||
| attrs.insert(name.to_owned(), vec![u8::from(value)]); | ||
| } | ||
|
|
||
| fn insert_u32(attrs: &mut AttributeMap, name: &'static str, value: u32) { | ||
| attrs.insert(name.to_owned(), value.to_be_bytes().to_vec()); | ||
| } | ||
|
|
||
| fn insert_u64(attrs: &mut AttributeMap, name: &'static str, value: u64) { | ||
| attrs.insert(name.to_owned(), value.to_be_bytes().to_vec()); | ||
| } | ||
|
|
||
| fn insert_u128(attrs: &mut AttributeMap, name: &'static str, value: u128) { | ||
| attrs.insert(name.to_owned(), value.to_be_bytes().to_vec()); | ||
| } | ||
|
|
||
| fn insert_u256(attrs: &mut AttributeMap, name: &'static str, value: U256) { | ||
| let mut out = vec![0u8; 32]; | ||
| value | ||
| .to_be_bytes_vec() | ||
| .iter() | ||
| .rev() | ||
| .take(32) | ||
| .enumerate() | ||
| .for_each(|(idx, byte)| { | ||
| out[31 - idx] = *byte; | ||
| }); | ||
| attrs.insert(name.to_owned(), out); | ||
| } |
There was a problem hiding this comment.
I don’t think these need separate functions. They’re only used once, so we can just initialize them inline when creating the hashmap.
| #[derive(Clone, Debug, Default, PartialEq, Eq)] | ||
| pub struct StateDelta { | ||
| pub updated_attributes: AttributeMap, | ||
| } |
There was a problem hiding this comment.
I don’t think this struct is needed either.
Use ABI-generated event decoding, add initial state defaults, wire component store lookups, support configured multi-pool indexing, and complete LunarBase native simulation transition/limits/swap query behavior.
69e6db4 to
a68cb27
Compare
No description provided.