feat(solana-indexer): PR 7.3 decode settlements via the shared interface parser - #4666
feat(solana-indexer): PR 7.3 decode settlements via the shared interface parser#4666squadgazzz wants to merge 10 commits into
Conversation
|
Claude finished @squadgazzz's task in 3m 15s —— View job PR Review: decode settlements via the shared interface parser
This is a clean simplification — dropping the hand-rolled One substantive point, left as an inline comment:
Nits I did not flag (already acknowledged / fine): the I couldn't compile locally (the |
|
This pull request has been marked as stale because it has been inactive a while. Please update this pull request or it will be automatically closed. |
The parsing-generic branch is merged and its ref deleted, so the branch pin would break the next cargo update. Main's interface types the push amount as u64, dropping the byte-array decode on our side.
| /// `SettlementFinalized`: the auction id read from the begin wire, the summed | ||
| /// sell amount, the buy-side push amount paired to its order by position | ||
| /// (order `i` is paid by push `i`), the order UID from the injected resolver, | ||
| /// and the solver read as the fee payer. |
There was a problem hiding this comment.
This is super hard to understand. Seems like a bullet list where each point is a bit more verbose would be better. If that is even needed here.
| // BeginSettle body: finalize index 1, one order, bump 0xAA, two transfers of | ||
| // 300 and 700 (sum 1000 = the sell-side amount withdrawn). | ||
| // BeginSettle body: finalize index 1, auction id 4242, one order, bump 0xAA, | ||
| // two transfers of 300 and 700 (sum 1000 = the sell-side amount withdrawn). |
There was a problem hiding this comment.
Why do we sum those amounts together? Aren't they separate things? Can it happen that the associated orders don't have the same tokens?
There was a problem hiding this comment.
Each order inside the BeginSettle instruction has its own list of transfers, all taking tokens from that order's sell token account, and the program lets the solver pick any destinations for them. So the sum is one order's total. For example, an order sells 1000 USDC, the solver sends 700 to its own account for the swap and 300 to the buffer as a fee or something. The on-chain counter for that order grows by 1000 either way, and the TradeDelta must match it. A second order is a separate entry with its own list, so amounts of different orders never sum together.
Updated the comment
| begin_data.extend_from_slice(&1u16.to_le_bytes()); | ||
| begin_data.extend_from_slice(&4242i64.to_le_bytes()); |
There was a problem hiding this comment.
I thought we would have a proper crate for constructing those interactions. Is this not done yet?
There was a problem hiding this comment.
The API was pretty unstable, but right now, we can probably try switching to that. Updated in ad3c4e2.
| SettlementInstruction::BeginSettle | SettlementInstruction::FinalizeSettle => { | ||
| Ok(Vec::new()) | ||
| } | ||
| // No domain event. |
There was a problem hiding this comment.
what does this comment mean? Do we not care about this event - and if so why?
Also why does it make sense for this variant to get its own branch instead of being added to the branch above which also returns Ok(Vec::new())?
There was a problem hiding this comment.
Initialize is the one-time setup instruction that creates the program's state account at deployment. IIUC, nothing order-related happens there, so there is nothing to store. ReclaimOrder closes an order account, and we probably should store that as an OrderClosed event, but that mapping is not built, so for now it also emits nothing. Added a TODO comment.
| let Some(finalize) = instructions.iter().find(|instruction| { | ||
| instruction.instruction_index == u32::from(begin_input.finalize_ix_index) |
There was a problem hiding this comment.
Why do we iterate over the whole instruction vector when the begin settle data already tells us the index we should look at? Is it not guaranteed that instructions[n].instruction_index == n?
There was a problem hiding this comment.
As I understand, the vec only holds our program's instructions, so positions shift. For example, the transaction is [0: ComputeBudget, 1: BeginSettle, 2: Jupiter swap, 3: FinalizeSettle]. Our filtered vec is [BeginSettle, FinalizeSettle] at positions 0 and 1, but begin says finalize_ix_index = 3, its position in the original transaction. So instructions[3] would be out of bounds, and the find matches the stored instruction_index field instead. With just 2-3 entries, a map is probably not worth it.
| .pushes | ||
| .iter() | ||
| .map(|push| push.amount) | ||
| .collect(); |
There was a problem hiding this comment.
Does not look like this collect is actually needed.
There was a problem hiding this comment.
Makes sense, dropped.
| .iter() | ||
| .zip(received) | ||
| .filter_map(|(order, amount_received_delta)| { | ||
| let resolved = resolve_order(order.order_pda)?; |
There was a problem hiding this comment.
was already here before the PR but it's unclear to me why resolve_order needs to be passed in a a closure instead of being a normal function call.
There was a problem hiding this comment.
Since the instruction only contains the order's PDA address, to build a TradeDelta, we also need the order UID, and that lives in our database. So decode_settlement takes a "give me the order for this PDA" function as a parameter. For tests, a two-line stub is used, while for production, we will pass a DB query. If it called the DB directly, every decoder test would need a running Postgres.
| .filter_map(|(order, amount_received_delta)| { | ||
| let resolved = resolve_order(order.order_pda)?; | ||
| // Sell-side pull total. Amounts are little-endian `u64`, and | ||
| // the stream is untrusted, so saturate instead of wrapping. |
There was a problem hiding this comment.
bit odd to point out that the stream is untrusted when zip already assumes that the pairings match up correctly.
There was a problem hiding this comment.
Overflow now invalidates the pair, see below.
| .amounts | ||
| .iter() | ||
| .map(|amount| u64::from_le_bytes(*amount)) | ||
| .fold(0u64, u64::saturating_add); |
There was a problem hiding this comment.
if we overflow the u64 we should surface an error, no?
There was a problem hiding this comment.
Yeah, it now skips the pair with a warning instead.
…runcating The order/push zip silently truncated to the shorter side and the pull sum saturated. Both are layout violations, so the pair is skipped with a warning. Also merges the no-event instruction arms, drops the intermediate push collect, and inlines the test instruction helper.
The CreateOrder and BeginSettle/FinalizeSettle fixtures go through the client crate's builders as a dev-dependency, so the tests round-trip the real encoder into our parser instead of hand-rolling bytes. The invalid-instruction fixtures stay hand-rolled: they craft data no builder would produce, and they double as the wire-layout pin.
| if corrupt { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
You can avoid this by assigning a label to the outer loop with:
'process_instructions: for begin in instructions {
and then directly continuing that loop with:
continue 'process_instructions;
There was a problem hiding this comment.
Makes sense. Updated.
| auction_id: 4242, | ||
| orders: &[settlement_client::instructions::InitializedIntent { | ||
| intent: &intent, | ||
| pulls: &[ |
There was a problem hiding this comment.
The language changed from push to pull but the test comments still reference push which is confusing.
There was a problem hiding this comment.
Good catch! Reworded.
Description
The settlement decoder hand-rolled the
BeginSettleandFinalizeSettlewire layout and stubbed the auction id and the buy-side amounts. It had to: the sharedsettlement-interfaceparser needed an on-chainAccountView, which the indexer cannot get off-chain. cowprotocol/solana-programs#80 makes that parser generic over the account type, so the indexer now decodes with its ownPubkeytype and drops the duplicated layout and the placeholders.Changes
CreateOrder,CreateBuffer,BeginSettle, andFinalizeSettlethrough the interface parser.BeginSettle(was a placeholder).FinalizeSettlepushes (was a placeholder).iis paid by pushi, per the on-chain invariant.parse_begin_settle_ordersand the account-position constants.How to test
Updated unit tests.