From 8ebc9519351faed2b358600fc3a8d1ee15f338d4 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Fri, 24 Jul 2026 08:07:11 +0000 Subject: [PATCH 1/7] refactor(solana-indexer): decode settlements via shared interface parser --- Cargo.lock | 2 +- crates/solana-indexer/Cargo.toml | 2 +- crates/solana-indexer/src/indexer/decoder.rs | 267 +++++++----------- .../src/indexer/decoder/tests.rs | 48 ++-- 4 files changed, 134 insertions(+), 185 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ac4105e4a..4437b1008d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9082,7 +9082,7 @@ dependencies = [ [[package]] name = "settlement-interface" version = "0.1.0" -source = "git+https://github.com/cowprotocol/solana-programs?rev=4e0bd7eaed7c237a327705425be2a4a50a8b4a3a#4e0bd7eaed7c237a327705425be2a4a50a8b4a3a" +source = "git+https://github.com/cowprotocol/solana-programs?branch=make-parsing-generic-on-accounts#172927c9e00b588d29796a134700ffc7a79d0e8c" dependencies = [ "arrayref", "derive_more 1.0.0", diff --git a/crates/solana-indexer/Cargo.toml b/crates/solana-indexer/Cargo.toml index b73c8022f9..b4053175b2 100644 --- a/crates/solana-indexer/Cargo.toml +++ b/crates/solana-indexer/Cargo.toml @@ -23,7 +23,7 @@ futures = { workspace = true } settlement-interface = { package = "settlement-interface", git = "https://github.com/cowprotocol/solana-programs", - rev = "4e0bd7eaed7c237a327705425be2a4a50a8b4a3a" + branch = "make-parsing-generic-on-accounts" } solana-client = { workspace = true } solana-sdk = { workspace = true } diff --git a/crates/solana-indexer/src/indexer/decoder.rs b/crates/solana-indexer/src/indexer/decoder.rs index efbecf10d3..bc8c61612f 100644 --- a/crates/solana-indexer/src/indexer/decoder.rs +++ b/crates/solana-indexer/src/indexer/decoder.rs @@ -25,7 +25,12 @@ use { Pubkey as InterfacePubkey, SettlementInstruction, data::intent::EncodedOrderIntent, - instruction::settle::recover_counterpart, + instruction::{ + InstructionInputParsing, + create_buffer::CreateBufferInput, + create_order::CreateOrderInput, + settle::{BeginSettleInput, FinalizeSettleInput}, + }, recover_discriminator, }, solana_sdk::pubkey::Pubkey, @@ -146,30 +151,6 @@ impl Decoder { } } -/// Auction id is not carried on the `BeginSettle` wire yet, so every -/// `SettlementFinalized` uses this fixed placeholder. -// TODO: not in the BeginSettle wire yet; use the real value once the program -// emits it. -const PLACEHOLDER_AUCTION_ID: u64 = 0; - -/// The buy-side push amount is not carried on the `FinalizeSettle` wire yet, so -/// every trade's `amount_received_delta` uses this fixed placeholder. -// TODO: FinalizeSettle carries no push amount yet; use the real value once it -// does. -const PLACEHOLDER_AMOUNT_RECEIVED: u64 = 0; - -/// Position of the `created_by` account in a `CreateOrder`'s account list -/// `[owner (S), created_by (W,S), order_pda (W), system_program (R)]`. -const CREATE_ORDER_CREATED_BY: usize = 1; - -/// Accounts a `BeginSettle` carries before its per-order accounts: -/// `[instructions_sysvar, state_pda, token_program]`. -const BEGIN_SETTLE_FIXED_ACCOUNTS: usize = 3; - -/// Accounts a `CreateBuffer` carries before its per-buffer `(buffer_pda, mint)` -/// pairs: `[payer, system_program, token_program]`. -const CREATE_BUFFER_SHARED_ACCOUNTS: usize = 3; - /// Order fields the `BeginSettle` wire does not carry, looked up per order PDA /// through an injected resolver so the decode stays a pure function. A future /// PR backs the resolver with the persisted order rows. @@ -197,7 +178,7 @@ fn decode_settlement( // Instructions decodable on their own, without tx-level pairing. for instruction in instructions { - let Ok((discriminator, body)) = recover_discriminator(&instruction.data) else { + let Ok((discriminator, _)) = recover_discriminator(&instruction.data) else { tracing::debug!( instruction_index = instruction.instruction_index, "settlement instruction with an unknown discriminator, skipping" @@ -211,7 +192,7 @@ fn decode_settlement( // the dead-letter table. let decoded = match discriminator { SettlementInstruction::CreateOrder => { - decode_order_created(instruction, body, &ctx.account_keys).map(|event| vec![event]) + decode_order_created(instruction, &ctx.account_keys).map(|event| vec![event]) } SettlementInstruction::CreateBuffer => { decode_buffers_created(instruction, &ctx.account_keys) @@ -222,6 +203,8 @@ fn decode_settlement( SettlementInstruction::BeginSettle | SettlementInstruction::FinalizeSettle => { Ok(Vec::new()) } + // No domain event yet. + SettlementInstruction::ReclaimOrder => Ok(Vec::new()), }; match decoded { Ok(decoded_events) => events.extend(decoded_events), @@ -243,62 +226,49 @@ fn decode_settlement( events } -/// `CreateOrder` -> `OrderCreated`. The instruction data is the encoded order -/// intent: its hash is the order UID and it carries the owner. `created_by` is -/// resolved from the instruction's account list. +/// `CreateOrder` -> `OrderCreated`. The parser recovers the encoded order +/// intent and the `created_by` account; the intent's hash is the order UID and +/// it carries the owner. fn decode_order_created( instruction: &ResolvedInstruction, - body: &[u8], account_keys: &[Pubkey], ) -> Result { - let intent_bytes: &[u8; EncodedOrderIntent::SIZE] = - body.try_into().map_err(|_| DecodeError::SchemaMismatch)?; - let (intent, uid) = EncodedOrderIntent::decode_and_hash(intent_bytes) + let mut accounts = instruction_account_keys(instruction, account_keys)?; + let input = CreateOrderInput::parse(&instruction.data, &mut accounts) + .map_err(|_| DecodeError::SchemaMismatch)?; + let (intent, uid) = EncodedOrderIntent::decode_and_hash(&input.intent_bytes) .map_err(|_| DecodeError::SchemaMismatch)?; - let created_by = resolve_account(instruction, account_keys, CREATE_ORDER_CREATED_BY) - .ok_or(DecodeError::SchemaMismatch)?; Ok(SettlementEvent::OrderCreated { order_uid: OrderUid(uid.to_bytes()), owner: to_sdk_pubkey(intent.owner), - created_by, + created_by: *input.created_by, }) } -/// `CreateBuffer` -> one `BufferCreated` per created buffer. The wire body is -/// empty; each buffer is a trailing `(buffer_pda, mint)` account pair after the -/// shared accounts, and the event's token is the buffer's mint. +/// `CreateBuffer` -> one `BufferCreated` per created buffer. The parser groups +/// the trailing accounts into `[buffer_pda, mint]` pairs; the event's token is +/// each pair's mint. fn decode_buffers_created( instruction: &ResolvedInstruction, account_keys: &[Pubkey], ) -> Result, DecodeError> { - let per_buffer = instruction - .accounts - .get(CREATE_BUFFER_SHARED_ACCOUNTS..) - .ok_or(DecodeError::SchemaMismatch)?; - let (pairs, remainder) = per_buffer.as_chunks::<2>(); - if !remainder.is_empty() || pairs.is_empty() { - return Err(DecodeError::SchemaMismatch); - } - pairs + let mut accounts = instruction_account_keys(instruction, account_keys)?; + let input = CreateBufferInput::parse(&instruction.data, &mut accounts) + .map_err(|_| DecodeError::SchemaMismatch)?; + Ok(input + .buffers .iter() - .map(|pair| { - // Each pair is `[buffer_pda_index, mint_index]`; the event token is - // the buffer's mint. - let token = account_keys - .get(usize::from(pair[1])) - .ok_or(DecodeError::SchemaMismatch)?; - Ok(SettlementEvent::BufferCreated { token: *token }) - }) - .collect() + .map(|pair| SettlementEvent::BufferCreated { token: pair[1] }) + .collect()) } /// Pair each `BeginSettle` with the `FinalizeSettle` it names and emit one /// `SettlementFinalized` per pair. /// -/// Pairing is by index: a `BeginSettle` body carries the top-level instruction -/// index of its `FinalizeSettle` (recovered via [`recover_counterpart`]), which -/// must match a `FinalizeSettle` present in the same transaction. It is -/// independent of the two instructions' relative order. +/// Pairing is by index: a parsed `BeginSettle` carries the top-level +/// instruction index of its `FinalizeSettle` (`finalize_ix_index`), which must +/// match a `FinalizeSettle` present in the same transaction. It is independent +/// of the two instructions' relative order. fn decode_settlements_finalized( instructions: &[ResolvedInstruction], ctx: &TxContext, @@ -312,57 +282,90 @@ fn decode_settlements_finalized( let mut events = Vec::new(); for begin in instructions { - let Ok((SettlementInstruction::BeginSettle, body)) = recover_discriminator(&begin.data) - else { + let Ok((SettlementInstruction::BeginSettle, _)) = recover_discriminator(&begin.data) else { continue; }; - // Body: `[finalize_ix_index: u16 BE][n][bump×n][count×n][amount×T]`. - // Peel the counterpart index, leaving the per-order pull layout. - let Ok((finalize_ix_index, pull_body)) = recover_counterpart(body) else { - continue; + let mut begin_accounts = match instruction_account_keys(begin, &ctx.account_keys) { + Ok(accounts) => accounts, + Err(_) => continue, }; + let begin_input = match BeginSettleInput::parse(&begin.data, &mut begin_accounts) { + Ok(input) => input, + Err(_) => { + tracing::warn!( + instruction_index = begin.instruction_index, + "BeginSettle did not match the expected layout, skipping" + ); + continue; + } + }; + // The named `FinalizeSettle` must actually be present in this tx. - let paired = instructions.iter().any(|instruction| { - instruction.instruction_index == u32::from(finalize_ix_index) + let Some(finalize) = instructions.iter().find(|instruction| { + instruction.instruction_index == u32::from(begin_input.finalize_ix_index) && matches!( recover_discriminator(&instruction.data), Ok((SettlementInstruction::FinalizeSettle, _)) ) - }); - if !paired { + }) else { tracing::debug!( instruction_index = begin.instruction_index, - finalize_ix_index, + finalize_ix_index = begin_input.finalize_ix_index, "BeginSettle without a paired FinalizeSettle in the tx, skipping" ); continue; - } - - let Some(orders) = parse_begin_settle_orders(pull_body) else { - tracing::warn!( - instruction_index = begin.instruction_index, - "BeginSettle body did not match the expected pull layout, skipping" - ); - continue; }; + let mut finalize_accounts = match instruction_account_keys(finalize, &ctx.account_keys) { + Ok(accounts) => accounts, + Err(_) => continue, + }; + let finalize_input = + match FinalizeSettleInput::parse(&finalize.data, &mut finalize_accounts) { + Ok(input) => input, + Err(_) => { + tracing::warn!( + instruction_index = finalize.instruction_index, + "FinalizeSettle did not match the expected layout, skipping" + ); + continue; + } + }; + // Orders and finalize pushes are positionally aligned: `BeginSettle` + // enforces exactly one push per order, both sorted by order PDA, so order + // `i` is paid by push `i`. Collect the push amounts up front so the + // finalize borrow ends before the zip below. + let received: Vec = finalize_input + .pushes + .iter() + .map(|push| u64::from_le_bytes(*push.amount)) + .collect(); - let trades = orders - .into_iter() - .filter_map(|order| { - let order_pda = - resolve_account(begin, &ctx.account_keys, order.order_pda_position)?; - let resolved = resolve_order(&order_pda)?; + let trades = begin_input + .orders + .iter() + .zip(received) + .filter_map(|(order, amount_received_delta)| { + let resolved = resolve_order(order.order_pda)?; + // Sell-side pull total. Amounts are little-endian `u64`; the + // stream is untrusted, so saturate instead of wrapping. + let amount_withdrawn_delta = order + .amounts + .iter() + .map(|amount| u64::from_le_bytes(*amount)) + .fold(0u64, u64::saturating_add); Some(TradeDelta { order_uid: resolved.order_uid, - amount_withdrawn_delta: order.amount_withdrawn_delta, - amount_received_delta: PLACEHOLDER_AMOUNT_RECEIVED, + amount_withdrawn_delta, + amount_received_delta, order_fulfilled: resolved.order_fulfilled, }) }) .collect(); events.push(SettlementEvent::SettlementFinalized { - auction_id: PLACEHOLDER_AUCTION_ID, + // The wire carries `auction_id` as i64; it is non-negative in + // practice. + auction_id: begin_input.auction_id as u64, solver, tx_signature: ctx.signature, slot: ctx.slot, @@ -372,77 +375,23 @@ fn decode_settlements_finalized( events } -/// One settled order recovered from a `BeginSettle` body: where its order PDA -/// sits in the instruction's account list, and the sell amount pulled for it. -struct BeginSettleOrder { - /// Position of the order's PDA within the instruction's account list. - order_pda_position: usize, - /// Sum of the order's pull amounts: the sell-side `amount_withdrawn` delta. - amount_withdrawn_delta: u64, -} - -/// Hand-parse a `BeginSettle` body (after the counterpart index) into per-order -/// pull totals and the account position of each order's PDA. -// -// TODO: this duplicates the `BeginSettle` wire layout owned by -// `settlement_interface` (`[n][bump×n][count×n][amount: u64 BE ×T]`); the -// interface's own parser is account-coupled, so there is no data-only helper to -// call yet. Replace this once one exists. The layout also shifts when the -// program adds `auction_id` to the wire, so this must move in lockstep. -fn parse_begin_settle_orders(body: &[u8]) -> Option> { - let (&order_count, rest) = body.split_first()?; - let order_count = usize::from(order_count); - // Bumps are on-chain PDA-derivation input the indexer does not need. - let (_bumps, rest) = rest.split_at_checked(order_count)?; - let (counts, amount_bytes) = rest.split_at_checked(order_count)?; - let (amounts, remainder) = amount_bytes.as_chunks::<8>(); - if !remainder.is_empty() { - return None; - } - // The per-order transfer counts must sum to the number of amounts, so every - // destination pairs with exactly one amount. - let counts_sum = counts - .iter() - .map(|&count| usize::from(count)) - .sum::(); - if counts_sum != amounts.len() { - return None; - } - - let mut orders = Vec::with_capacity(order_count); - let mut account_position = BEGIN_SETTLE_FIXED_ACCOUNTS; - let mut amount_index = 0usize; - for &count in counts { - let count = usize::from(count); - let mut amount_withdrawn_delta = 0u64; - for amount in &amounts[amount_index..amount_index + count] { - // On-chain amounts are already u64, so a sum from a single sell - // account cannot exceed u64. Guard anyway: the stream is untrusted. - amount_withdrawn_delta = - amount_withdrawn_delta.checked_add(u64::from_be_bytes(*amount))?; - } - amount_index += count; - orders.push(BeginSettleOrder { - order_pda_position: account_position, - amount_withdrawn_delta, - }); - // Each order occupies its order PDA, its sell token account, and one - // destination per transfer. - account_position += 2 + count; - } - Some(orders) -} - -/// Resolve the account at `position` in the instruction's account list to its -/// pubkey, returning `None` if the position or the resolved index is out of -/// range. -fn resolve_account( +/// Resolve an instruction's account-list indices to their pubkeys, in order, so +/// the interface parser can read them positionally. Fails if any index is out +/// of range against the transaction's account list. +fn instruction_account_keys( instruction: &ResolvedInstruction, account_keys: &[Pubkey], - position: usize, -) -> Option { - let index = *instruction.accounts.get(position)?; - account_keys.get(usize::from(index)).copied() +) -> Result, DecodeError> { + instruction + .accounts + .iter() + .map(|&index| { + account_keys + .get(usize::from(index)) + .copied() + .ok_or(DecodeError::SchemaMismatch) + }) + .collect() } /// Bridge a `settlement_interface` pubkey to the indexer's `solana_sdk` pubkey. diff --git a/crates/solana-indexer/src/indexer/decoder/tests.rs b/crates/solana-indexer/src/indexer/decoder/tests.rs index e796863487..29fb201076 100644 --- a/crates/solana-indexer/src/indexer/decoder/tests.rs +++ b/crates/solana-indexer/src/indexer/decoder/tests.rs @@ -1,13 +1,5 @@ use { - super::{ - Decoder, - PLACEHOLDER_AMOUNT_RECEIVED, - PLACEHOLDER_AUCTION_ID, - ResolvedOrder, - build_account_keys, - decode_settlement, - relevant_instructions, - }, + super::{Decoder, ResolvedOrder, build_account_keys, decode_settlement, relevant_instructions}, crate::{ persistence::Persistence, types::{ @@ -355,9 +347,10 @@ fn create_order_decodes_to_order_created() { } /// A crafted `BeginSettle` + `FinalizeSettle` pair decodes to one -/// `SettlementFinalized`: the real summed sell amount and the resolved order -/// UID (via the injected map), with the auction id and buy-side amount left at -/// their documented placeholders and the solver read as the fee payer. +/// `SettlementFinalized`: the real auction id read from the begin wire, the +/// summed sell amount, the buy-side push amount matched to the order by +/// destination, and the resolved order UID (via the injected map), with the +/// solver read as the fee payer. #[test] fn begin_and_finalize_settle_decode_to_settlement_finalized() { let (settlement, solflow) = (pubkey(1), pubkey(2)); @@ -365,7 +358,7 @@ fn begin_and_finalize_settle_decode_to_settlement_finalized() { let order_pda = pubkey(20); // Account list: // [solver(0), settlement(1), sysvar(2), state(3), token(4), order_pda(5), - // sell(6), dest0(7), dest1(8)]. + // sell(6), dest0(7), dest1(8), buffer(9)]. let account_keys = vec![ solver, settlement, @@ -376,21 +369,28 @@ fn begin_and_finalize_settle_decode_to_settlement_finalized() { pubkey(26), pubkey(27), pubkey(28), + pubkey(29), ]; - // 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). + // The wire is little-endian, matching the interface's encoder. let mut begin_data = vec![SettlementInstruction::BeginSettle.discriminator()]; - begin_data.extend_from_slice(&1u16.to_be_bytes()); + begin_data.extend_from_slice(&1u16.to_le_bytes()); + begin_data.extend_from_slice(&4242i64.to_le_bytes()); begin_data.push(1); begin_data.push(0xAA); begin_data.push(2); - begin_data.extend_from_slice(&300u64.to_be_bytes()); - begin_data.extend_from_slice(&700u64.to_be_bytes()); + begin_data.extend_from_slice(&300u64.to_le_bytes()); + begin_data.extend_from_slice(&700u64.to_le_bytes()); - // FinalizeSettle body: begin index 0. + // FinalizeSettle body: begin index 0, one push of 1234 to dest0 (bump 0xBB). + // dest0 is one of the order's begin destinations, so it credits the order's + // buy-side receipt. let mut finalize_data = vec![SettlementInstruction::FinalizeSettle.discriminator()]; - finalize_data.extend_from_slice(&0u16.to_be_bytes()); + finalize_data.extend_from_slice(&0u16.to_le_bytes()); + finalize_data.push(0xBB); + finalize_data.extend_from_slice(&1_234u64.to_le_bytes()); let tx = tx_info( account_keys, @@ -399,8 +399,8 @@ fn begin_and_finalize_settle_decode_to_settlement_finalized() { vec![ // BeginSettle @ 0: sysvar, state, token, order_pda, sell, dest0, dest1. compiled(1, vec![2, 3, 4, 5, 6, 7, 8], begin_data), - // FinalizeSettle @ 1: sysvar only. - compiled(1, vec![2], finalize_data), + // FinalizeSettle @ 1: sysvar, state, token, buffer (source), dest0. + compiled(1, vec![2, 3, 4, 9, 7], finalize_data), ], vec![], ); @@ -425,14 +425,14 @@ fn begin_and_finalize_settle_decode_to_settlement_finalized() { assert_eq!( events, vec![SettlementEvent::SettlementFinalized { - auction_id: PLACEHOLDER_AUCTION_ID, + auction_id: 4242, solver, tx_signature: signature(6), slot: Slot(5), trades: vec![TradeDelta { order_uid: expected_uid, amount_withdrawn_delta: 1_000, - amount_received_delta: PLACEHOLDER_AMOUNT_RECEIVED, + amount_received_delta: 1_234, order_fulfilled: true, }], }] From 138a48d5aaab9a5b663de794ff5cd8346726ea34 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Fri, 24 Jul 2026 12:21:23 +0000 Subject: [PATCH 2/7] docs(solana-indexer): reword decode comments to describe current behavior --- crates/solana-indexer/src/indexer/decoder.rs | 2 +- crates/solana-indexer/src/indexer/decoder/tests.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/solana-indexer/src/indexer/decoder.rs b/crates/solana-indexer/src/indexer/decoder.rs index bc8c61612f..3d70b312fe 100644 --- a/crates/solana-indexer/src/indexer/decoder.rs +++ b/crates/solana-indexer/src/indexer/decoder.rs @@ -203,7 +203,7 @@ fn decode_settlement( SettlementInstruction::BeginSettle | SettlementInstruction::FinalizeSettle => { Ok(Vec::new()) } - // No domain event yet. + // No domain event. SettlementInstruction::ReclaimOrder => Ok(Vec::new()), }; match decoded { diff --git a/crates/solana-indexer/src/indexer/decoder/tests.rs b/crates/solana-indexer/src/indexer/decoder/tests.rs index 29fb201076..138ff6db28 100644 --- a/crates/solana-indexer/src/indexer/decoder/tests.rs +++ b/crates/solana-indexer/src/indexer/decoder/tests.rs @@ -287,7 +287,7 @@ async fn run_drains_transactions_until_the_sender_drops() { assert!(decoder.run().await.is_ok()); } -/// A crafted `CreateOrder` decodes to `OrderCreated` with the real UID (the +/// A crafted `CreateOrder` decodes to `OrderCreated` with the UID (the /// hash of the encoded intent), the intent's owner, and the `created_by` /// account resolved from the instruction's account list. The account-list owner /// differs from the intent owner, so this also pins that the event owner comes @@ -347,10 +347,10 @@ fn create_order_decodes_to_order_created() { } /// A crafted `BeginSettle` + `FinalizeSettle` pair decodes to one -/// `SettlementFinalized`: the real auction id read from the begin wire, the -/// summed sell amount, the buy-side push amount matched to the order by -/// destination, and the resolved order UID (via the injected map), with the -/// solver read as the fee payer. +/// `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. #[test] fn begin_and_finalize_settle_decode_to_settlement_finalized() { let (settlement, solflow) = (pubkey(1), pubkey(2)); From 1e6439fa04cbd1f0f30ecc730d5d642888eca10a Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Tue, 4 Aug 2026 12:55:18 +0000 Subject: [PATCH 3/7] Pin settlement-interface to solana-programs main 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. --- Cargo.lock | 4 ++-- crates/solana-indexer/Cargo.toml | 2 +- crates/solana-indexer/src/indexer/decoder.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4437b1008d..48026b4702 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4275,7 +4275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.118", + "syn 1.0.109", ] [[package]] @@ -9082,7 +9082,7 @@ dependencies = [ [[package]] name = "settlement-interface" version = "0.1.0" -source = "git+https://github.com/cowprotocol/solana-programs?branch=make-parsing-generic-on-accounts#172927c9e00b588d29796a134700ffc7a79d0e8c" +source = "git+https://github.com/cowprotocol/solana-programs?rev=4d4fc46f594e0159246afaf148f0af2a463d0095#4d4fc46f594e0159246afaf148f0af2a463d0095" dependencies = [ "arrayref", "derive_more 1.0.0", diff --git a/crates/solana-indexer/Cargo.toml b/crates/solana-indexer/Cargo.toml index b4053175b2..780e24b219 100644 --- a/crates/solana-indexer/Cargo.toml +++ b/crates/solana-indexer/Cargo.toml @@ -23,7 +23,7 @@ futures = { workspace = true } settlement-interface = { package = "settlement-interface", git = "https://github.com/cowprotocol/solana-programs", - branch = "make-parsing-generic-on-accounts" + rev = "4d4fc46f594e0159246afaf148f0af2a463d0095" } solana-client = { workspace = true } solana-sdk = { workspace = true } diff --git a/crates/solana-indexer/src/indexer/decoder.rs b/crates/solana-indexer/src/indexer/decoder.rs index 3d70b312fe..c9c8bc6a8d 100644 --- a/crates/solana-indexer/src/indexer/decoder.rs +++ b/crates/solana-indexer/src/indexer/decoder.rs @@ -337,7 +337,7 @@ fn decode_settlements_finalized( let received: Vec = finalize_input .pushes .iter() - .map(|push| u64::from_le_bytes(*push.amount)) + .map(|push| push.amount) .collect(); let trades = begin_input From d671baffd04f3e75cace8021e9383d2a1bf83ea7 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Wed, 5 Aug 2026 06:36:52 +0000 Subject: [PATCH 4/7] Drop semicolons and inference emphasis from decoder comments --- crates/solana-indexer/src/indexer/decoder.rs | 14 +++++++------- crates/solana-indexer/src/types/events.rs | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/solana-indexer/src/indexer/decoder.rs b/crates/solana-indexer/src/indexer/decoder.rs index c9c8bc6a8d..98b1075d27 100644 --- a/crates/solana-indexer/src/indexer/decoder.rs +++ b/crates/solana-indexer/src/indexer/decoder.rs @@ -227,8 +227,8 @@ fn decode_settlement( } /// `CreateOrder` -> `OrderCreated`. The parser recovers the encoded order -/// intent and the `created_by` account; the intent's hash is the order UID and -/// it carries the owner. +/// intent and the `created_by` account. The intent's hash is the order UID, +/// and the intent carries the owner. fn decode_order_created( instruction: &ResolvedInstruction, account_keys: &[Pubkey], @@ -246,8 +246,8 @@ fn decode_order_created( } /// `CreateBuffer` -> one `BufferCreated` per created buffer. The parser groups -/// the trailing accounts into `[buffer_pda, mint]` pairs; the event's token is -/// each pair's mint. +/// the trailing accounts into `[buffer_pda, mint]` pairs, and the event's +/// token is each pair's mint. fn decode_buffers_created( instruction: &ResolvedInstruction, account_keys: &[Pubkey], @@ -346,8 +346,8 @@ fn decode_settlements_finalized( .zip(received) .filter_map(|(order, amount_received_delta)| { let resolved = resolve_order(order.order_pda)?; - // Sell-side pull total. Amounts are little-endian `u64`; the - // stream is untrusted, so saturate instead of wrapping. + // Sell-side pull total. Amounts are little-endian `u64`, and + // the stream is untrusted, so saturate instead of wrapping. let amount_withdrawn_delta = order .amounts .iter() @@ -363,7 +363,7 @@ fn decode_settlements_finalized( .collect(); events.push(SettlementEvent::SettlementFinalized { - // The wire carries `auction_id` as i64; it is non-negative in + // The wire carries `auction_id` as i64. It is non-negative in // practice. auction_id: begin_input.auction_id as u64, solver, diff --git a/crates/solana-indexer/src/types/events.rs b/crates/solana-indexer/src/types/events.rs index fd56a66d3a..ff0d3587ec 100644 --- a/crates/solana-indexer/src/types/events.rs +++ b/crates/solana-indexer/src/types/events.rs @@ -23,8 +23,8 @@ pub(crate) struct TradeDelta { pub amount_received_delta: u64, /// Whether the order is fully filled after this trade. /// - /// This is **not** a field emitted by the settlement program's event data; - /// it is inferred by the decoder from the order PDA's post-trade snapshot. + /// Not a field of the program's event data: the decoder infers it from + /// the order PDA's post-trade snapshot. /// It is `true` when post-trade `amount_withdrawn` equals the order's full /// sell amount, or `amount_received` equals the full buy amount. pub order_fulfilled: bool, From f0b4a829d64f0b60f23d92614feafc0762a71c33 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Wed, 5 Aug 2026 08:36:50 +0000 Subject: [PATCH 5/7] Reject count-mismatched and overflowing settlement pairs instead of truncating 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. --- crates/solana-indexer/src/indexer/decoder.rs | 81 +++++++++------- .../src/indexer/decoder/tests.rs | 93 +++++++++++++------ 2 files changed, 117 insertions(+), 57 deletions(-) diff --git a/crates/solana-indexer/src/indexer/decoder.rs b/crates/solana-indexer/src/indexer/decoder.rs index 98b1075d27..d2705a6db4 100644 --- a/crates/solana-indexer/src/indexer/decoder.rs +++ b/crates/solana-indexer/src/indexer/decoder.rs @@ -197,14 +197,15 @@ fn decode_settlement( SettlementInstruction::CreateBuffer => { decode_buffers_created(instruction, &ctx.account_keys) } - // Bootstrap only, no domain event. - SettlementInstruction::Initialize => Ok(Vec::new()), // Paired below, once both halves of the settlement are in hand. SettlementInstruction::BeginSettle | SettlementInstruction::FinalizeSettle => { Ok(Vec::new()) } - // No domain event. - SettlementInstruction::ReclaimOrder => Ok(Vec::new()), + // No domain event: `Initialize` bootstraps the program state. + // TODO: map `ReclaimOrder` to `OrderClosed`. + SettlementInstruction::Initialize | SettlementInstruction::ReclaimOrder => { + Ok(Vec::new()) + } }; match decoded { Ok(decoded_events) => events.extend(decoded_events), @@ -331,36 +332,54 @@ fn decode_settlements_finalized( } }; // Orders and finalize pushes are positionally aligned: `BeginSettle` - // enforces exactly one push per order, both sorted by order PDA, so order - // `i` is paid by push `i`. Collect the push amounts up front so the - // finalize borrow ends before the zip below. - let received: Vec = finalize_input - .pushes - .iter() - .map(|push| push.amount) - .collect(); + // enforces exactly one push per order, both sorted by order PDA, so + // order `i` is paid by push `i`. A count mismatch breaks that + // invariant, so the pair cannot be decoded. + let order_count = begin_input.orders.iter().count(); + let push_count = finalize_input.pushes.iter().count(); + if order_count != push_count { + tracing::warn!( + instruction_index = begin.instruction_index, + order_count, + push_count, + "order and push counts differ, skipping the settlement pair" + ); + continue; + } - let trades = begin_input + let mut trades = Vec::with_capacity(order_count); + let mut corrupt = false; + for (order, amount_received_delta) in begin_input .orders .iter() - .zip(received) - .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. - let amount_withdrawn_delta = order - .amounts - .iter() - .map(|amount| u64::from_le_bytes(*amount)) - .fold(0u64, u64::saturating_add); - Some(TradeDelta { - order_uid: resolved.order_uid, - amount_withdrawn_delta, - amount_received_delta, - order_fulfilled: resolved.order_fulfilled, - }) - }) - .collect(); + .zip(finalize_input.pushes.iter().map(|push| push.amount)) + { + let Some(resolved) = resolve_order(order.order_pda) else { + continue; + }; + // Sell-side pull total, little-endian on the wire. An overflowing + // sum cannot be a real settlement, so it invalidates the pair. + let sum = order.amounts.iter().try_fold(0u64, |acc, amount| { + acc.checked_add(u64::from_le_bytes(*amount)) + }); + let Some(amount_withdrawn_delta) = sum else { + tracing::warn!( + instruction_index = begin.instruction_index, + "pull amounts overflow u64, skipping the settlement pair" + ); + corrupt = true; + break; + }; + trades.push(TradeDelta { + order_uid: resolved.order_uid, + amount_withdrawn_delta, + amount_received_delta, + order_fulfilled: resolved.order_fulfilled, + }); + } + if corrupt { + continue; + } events.push(SettlementEvent::SettlementFinalized { // The wire carries `auction_id` as i64. It is non-negative in diff --git a/crates/solana-indexer/src/indexer/decoder/tests.rs b/crates/solana-indexer/src/indexer/decoder/tests.rs index 138ff6db28..ec83ad5f6d 100644 --- a/crates/solana-indexer/src/indexer/decoder/tests.rs +++ b/crates/solana-indexer/src/indexer/decoder/tests.rs @@ -38,14 +38,6 @@ fn key_bytes(key: Pubkey) -> Vec { key.to_bytes().to_vec() } -fn compiled(program_id_index: u32, accounts: Vec, data: Vec) -> CompiledInstruction { - CompiledInstruction { - program_id_index, - accounts, - data, - } -} - fn inner( program_id_index: u32, accounts: Vec, @@ -104,8 +96,16 @@ fn resolves_settlement_and_solflow_across_top_level_and_cpi() { vec![solflow, acct_b], // top-level: a router call (dropped) then a solflow call (kept, index 1) vec![ - compiled(0, vec![1], vec![0]), - compiled(3, vec![1, 4], vec![1, 2, 3]), + CompiledInstruction { + program_id_index: 0, + accounts: vec![1], + data: vec![0], + }, + CompiledInstruction { + program_id_index: 3, + accounts: vec![1, 4], + data: vec![1, 2, 3], + }, ], // settlement invoked as a CPI under top-level instruction 0 vec![InnerInstructions { @@ -154,11 +154,23 @@ fn unresolvable_programs_dropped_account_indices_carried_through() { account_keys: vec![key_bytes(settlement), vec![1, 2, 3, 4, 5]], instructions: vec![ // program index 9 is out of range -> dropped - compiled(9, vec![0], vec![0]), + CompiledInstruction { + program_id_index: 9, + accounts: vec![0], + data: vec![0], + }, // program index 1 is the zeroed bad key -> untracked, dropped - compiled(1, vec![0], vec![0]), + CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![0], + }, // settlement, with an out-of-range account index carried as-is - compiled(0, vec![5], vec![7]), + CompiledInstruction { + program_id_index: 0, + accounts: vec![5], + data: vec![7], + }, ], ..Default::default() }), @@ -189,7 +201,11 @@ fn inner_ix_path_tracks_cpi_nesting_depth() { vec![], vec![], // one top-level router call (dropped) - vec![compiled(0, vec![4], vec![0])], + vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![4], + data: vec![0], + }], vec![InnerInstructions { index: 0, instructions: vec![ @@ -228,7 +244,11 @@ fn corrupt_stack_height_is_clamped() { vec![pubkey(9), settlement], // [router(0), settlement(1)] vec![], vec![], - vec![compiled(0, vec![1], vec![0])], // top-level router, dropped + vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![1], + data: vec![0], + }], // top-level router, dropped vec![InnerInstructions { index: 0, instructions: vec![inner(1, vec![1], vec![7], Some(10_000))], @@ -259,7 +279,11 @@ fn stream_tx(slot: Slot, signature: Signature, settlement: Pubkey) -> StreamUpda vec![settlement, pubkey(8)], vec![], vec![], - vec![compiled(0, vec![1], vec![0])], + vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![1], + data: vec![0], + }], vec![], ); StreamUpdate::Tx { @@ -323,7 +347,11 @@ fn create_order_decodes_to_order_created() { account_keys, vec![], vec![], - vec![compiled(0, vec![1, 2, 3, 4], data)], + vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![1, 2, 3, 4], + data, + }], vec![], ); @@ -347,10 +375,14 @@ fn create_order_decodes_to_order_created() { } /// A crafted `BeginSettle` + `FinalizeSettle` pair decodes to one -/// `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. +/// `SettlementFinalized`, where: +/// +/// - the auction id comes from the begin wire, +/// - the order's sell amount is the sum of its pulls, +/// - the push amount pairs to its order by position (order `i` is paid by push +/// `i`), +/// - the order UID comes from the injected resolver, +/// - the solver is the fee payer. #[test] fn begin_and_finalize_settle_decode_to_settlement_finalized() { let (settlement, solflow) = (pubkey(1), pubkey(2)); @@ -372,9 +404,10 @@ fn begin_and_finalize_settle_decode_to_settlement_finalized() { pubkey(29), ]; - // 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). - // The wire is little-endian, matching the interface's encoder. + // BeginSettle body: finalize index 1, auction id 4242, one order, bump + // 0xAA, and two pulls of 300 and 700. Both pulls drain the same order's + // sell token, so their sum (1000) is that order's withdrawn delta. The + // wire is little-endian, matching the interface's encoder. let mut begin_data = vec![SettlementInstruction::BeginSettle.discriminator()]; begin_data.extend_from_slice(&1u16.to_le_bytes()); begin_data.extend_from_slice(&4242i64.to_le_bytes()); @@ -398,9 +431,17 @@ fn begin_and_finalize_settle_decode_to_settlement_finalized() { vec![], vec![ // BeginSettle @ 0: sysvar, state, token, order_pda, sell, dest0, dest1. - compiled(1, vec![2, 3, 4, 5, 6, 7, 8], begin_data), + CompiledInstruction { + program_id_index: 1, + accounts: vec![2, 3, 4, 5, 6, 7, 8], + data: begin_data, + }, // FinalizeSettle @ 1: sysvar, state, token, buffer (source), dest0. - compiled(1, vec![2, 3, 4, 9, 7], finalize_data), + CompiledInstruction { + program_id_index: 1, + accounts: vec![2, 3, 4, 9, 7], + data: finalize_data, + }, ], vec![], ); From da359ae4fd2a90ad57c08867734ed49b30bff784 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Wed, 5 Aug 2026 09:34:45 +0000 Subject: [PATCH 6/7] Reword the settlement fixture comments in plain terms --- crates/solana-indexer/src/indexer/decoder/tests.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/solana-indexer/src/indexer/decoder/tests.rs b/crates/solana-indexer/src/indexer/decoder/tests.rs index ec83ad5f6d..05edfc83e0 100644 --- a/crates/solana-indexer/src/indexer/decoder/tests.rs +++ b/crates/solana-indexer/src/indexer/decoder/tests.rs @@ -377,7 +377,7 @@ fn create_order_decodes_to_order_created() { /// A crafted `BeginSettle` + `FinalizeSettle` pair decodes to one /// `SettlementFinalized`, where: /// -/// - the auction id comes from the begin wire, +/// - the auction id comes from the `BeginSettle` instruction data, /// - the order's sell amount is the sum of its pulls, /// - the push amount pairs to its order by position (order `i` is paid by push /// `i`), @@ -405,9 +405,10 @@ fn begin_and_finalize_settle_decode_to_settlement_finalized() { ]; // BeginSettle body: finalize index 1, auction id 4242, one order, bump - // 0xAA, and two pulls of 300 and 700. Both pulls drain the same order's - // sell token, so their sum (1000) is that order's withdrawn delta. The - // wire is little-endian, matching the interface's encoder. + // 0xAA, and two transfers of 300 and 700. Both take tokens from the same + // order's sell account, so their sum (1000) is that order's withdrawn + // delta. The byte layout is little-endian, matching the interface's + // encoder. let mut begin_data = vec![SettlementInstruction::BeginSettle.discriminator()]; begin_data.extend_from_slice(&1u16.to_le_bytes()); begin_data.extend_from_slice(&4242i64.to_le_bytes()); From ad3c4e24bc18983e3590cef495cfb93c4f6b08fd Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Wed, 5 Aug 2026 09:49:30 +0000 Subject: [PATCH 7/7] Build settlement test fixtures with the client crate 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. --- Cargo.lock | 9 + crates/solana-indexer/Cargo.toml | 7 + .../src/indexer/decoder/tests.rs | 184 +++++++++--------- 3 files changed, 109 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48026b4702..8cd0f10bc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9079,6 +9079,14 @@ dependencies = [ "serde", ] +[[package]] +name = "settlement-client" +version = "0.1.0" +source = "git+https://github.com/cowprotocol/solana-programs?rev=4d4fc46f594e0159246afaf148f0af2a463d0095#4d4fc46f594e0159246afaf148f0af2a463d0095" +dependencies = [ + "settlement-interface", +] + [[package]] name = "settlement-interface" version = "0.1.0" @@ -9948,6 +9956,7 @@ dependencies = [ "bytes", "derive_more 1.0.0", "futures", + "settlement-client", "settlement-interface", "solana-client", "solana-sdk", diff --git a/crates/solana-indexer/Cargo.toml b/crates/solana-indexer/Cargo.toml index 780e24b219..208fc4c30e 100644 --- a/crates/solana-indexer/Cargo.toml +++ b/crates/solana-indexer/Cargo.toml @@ -35,3 +35,10 @@ yellowstone-grpc-proto = { workspace = true } [lints] workspace = true + +[dev-dependencies] +settlement-client = { + package = "settlement-client", + git = "https://github.com/cowprotocol/solana-programs", + rev = "4d4fc46f594e0159246afaf148f0af2a463d0095" +} diff --git a/crates/solana-indexer/src/indexer/decoder/tests.rs b/crates/solana-indexer/src/indexer/decoder/tests.rs index 05edfc83e0..050866caa8 100644 --- a/crates/solana-indexer/src/indexer/decoder/tests.rs +++ b/crates/solana-indexer/src/indexer/decoder/tests.rs @@ -25,6 +25,7 @@ use { Pubkey as InterfacePubkey, SettlementInstruction, data::intent::{EncodedOrderIntent, OrderIntent, OrderKind}, + pda::order::find_order_pda, }, solana_sdk::pubkey::Pubkey, tokio::sync::mpsc::Sender, @@ -34,6 +35,36 @@ fn pubkey(n: u8) -> Pubkey { Pubkey::new_from_array([n; 32]) } +/// Compile client-built instructions into the proto transaction shape: the +/// account list starts with the fee payer, then every referenced key in +/// encounter order. +fn tx_from_instructions( + payer: Pubkey, + instructions: &[settlement_interface::Instruction], +) -> SubscribeUpdateTransactionInfo { + let mut keys = vec![payer]; + let index_of = |keys: &mut Vec, key: Pubkey| -> u8 { + if let Some(index) = keys.iter().position(|k| *k == key) { + return u8::try_from(index).unwrap(); + } + keys.push(key); + u8::try_from(keys.len() - 1).unwrap() + }; + let compiled = instructions + .iter() + .map(|instruction| CompiledInstruction { + program_id_index: u32::from(index_of(&mut keys, instruction.program_id)), + accounts: instruction + .accounts + .iter() + .map(|meta| index_of(&mut keys, meta.pubkey)) + .collect(), + data: instruction.data.clone(), + }) + .collect(); + tx_info(keys, vec![], vec![], compiled, vec![]) +} + fn key_bytes(key: Pubkey) -> Vec { key.to_bytes().to_vec() } @@ -311,21 +342,15 @@ async fn run_drains_transactions_until_the_sender_drops() { assert!(decoder.run().await.is_ok()); } -/// A crafted `CreateOrder` decodes to `OrderCreated` with the UID (the -/// hash of the encoded intent), the intent's owner, and the `created_by` -/// account resolved from the instruction's account list. The account-list owner -/// differs from the intent owner, so this also pins that the event owner comes -/// from the intent data, not the accounts. +/// A `CreateOrder` built by the client crate decodes to `OrderCreated` with +/// the UID (the hash of the encoded intent), the intent's owner, and the +/// `created_by` account resolved from the instruction's account list. The +/// account-list owner differs from the intent owner, so this also pins that +/// the event owner comes from the intent data, not the accounts. #[test] fn create_order_decodes_to_order_created() { let (settlement, solflow) = (pubkey(1), pubkey(2)); let created_by = pubkey(12); - // Account list: [settlement(0), owner(1), created_by(2), order_pda(3), - // system(4)]. - let account_keys = vec![settlement, pubkey(11), created_by, pubkey(13), pubkey(14)]; - - // Build the encoded intent through the interface's public API so the test - // hashes it independently of the decoder. let intent = OrderIntent { owner: InterfacePubkey::new_from_array([0x11; 32]), buy_token_account: InterfacePubkey::new_from_array([0x22; 32]), @@ -337,23 +362,14 @@ fn create_order_decodes_to_order_created() { partially_fillable: false, app_data: [0x44; 32], }; - let encoded = EncodedOrderIntent::from(&intent); - let intent_bytes: [u8; EncodedOrderIntent::SIZE] = (&encoded).into(); - let mut data = vec![SettlementInstruction::CreateOrder.discriminator()]; - data.extend_from_slice(&intent_bytes); - - // CreateOrder accounts: [owner, created_by, order_pda, system]. - let tx = tx_info( - account_keys, - vec![], - vec![], - vec![CompiledInstruction { - program_id_index: 0, - accounts: vec![1, 2, 3, 4], - data, - }], - vec![], - ); + let instruction = settlement_client::instructions::CreateOrder { + program_id: settlement, + owner: pubkey(11), + created_by, + intent: &intent, + } + .into(); + let tx = tx_from_instructions(pubkey(9), &[instruction]); let ctx = TxContext { slot: Slot(5), @@ -374,78 +390,64 @@ fn create_order_decodes_to_order_created() { ); } -/// A crafted `BeginSettle` + `FinalizeSettle` pair decodes to one -/// `SettlementFinalized`, where: +/// A `BeginSettle` + `FinalizeSettle` pair built by the client crate decodes +/// to one `SettlementFinalized`, where: /// /// - the auction id comes from the `BeginSettle` instruction data, -/// - the order's sell amount is the sum of its pulls, +/// - the order's sell amount is the sum of its pulls (300 + 700, both taken +/// from the same order's sell account), /// - the push amount pairs to its order by position (order `i` is paid by push /// `i`), -/// - the order UID comes from the injected resolver, +/// - the order UID comes from the injected resolver, keyed by the canonical +/// order PDA the builder derives, /// - the solver is the fee payer. #[test] fn begin_and_finalize_settle_decode_to_settlement_finalized() { let (settlement, solflow) = (pubkey(1), pubkey(2)); let solver = pubkey(10); - let order_pda = pubkey(20); - // Account list: - // [solver(0), settlement(1), sysvar(2), state(3), token(4), order_pda(5), - // sell(6), dest0(7), dest1(8), buffer(9)]. - let account_keys = vec![ - solver, - settlement, - pubkey(22), - pubkey(23), - pubkey(24), - order_pda, - pubkey(26), - pubkey(27), - pubkey(28), - pubkey(29), - ]; - - // BeginSettle body: finalize index 1, auction id 4242, one order, bump - // 0xAA, and two transfers of 300 and 700. Both take tokens from the same - // order's sell account, so their sum (1000) is that order's withdrawn - // delta. The byte layout is little-endian, matching the interface's - // encoder. - let mut begin_data = vec![SettlementInstruction::BeginSettle.discriminator()]; - begin_data.extend_from_slice(&1u16.to_le_bytes()); - begin_data.extend_from_slice(&4242i64.to_le_bytes()); - begin_data.push(1); - begin_data.push(0xAA); - begin_data.push(2); - begin_data.extend_from_slice(&300u64.to_le_bytes()); - begin_data.extend_from_slice(&700u64.to_le_bytes()); - - // FinalizeSettle body: begin index 0, one push of 1234 to dest0 (bump 0xBB). - // dest0 is one of the order's begin destinations, so it credits the order's - // buy-side receipt. - let mut finalize_data = vec![SettlementInstruction::FinalizeSettle.discriminator()]; - finalize_data.extend_from_slice(&0u16.to_le_bytes()); - finalize_data.push(0xBB); - finalize_data.extend_from_slice(&1_234u64.to_le_bytes()); - - let tx = tx_info( - account_keys, - vec![], - vec![], - vec![ - // BeginSettle @ 0: sysvar, state, token, order_pda, sell, dest0, dest1. - CompiledInstruction { - program_id_index: 1, - accounts: vec![2, 3, 4, 5, 6, 7, 8], - data: begin_data, - }, - // FinalizeSettle @ 1: sysvar, state, token, buffer (source), dest0. - CompiledInstruction { - program_id_index: 1, - accounts: vec![2, 3, 4, 9, 7], - data: finalize_data, - }, - ], - vec![], - ); + let intent = OrderIntent { + owner: InterfacePubkey::new_from_array([0x11; 32]), + buy_token_account: InterfacePubkey::new_from_array([0x22; 32]), + sell_token_account: InterfacePubkey::new_from_array([0x33; 32]), + sell_amount: 1_000, + buy_amount: 1_234, + valid_to: 42, + kind: OrderKind::Sell, + partially_fillable: false, + app_data: [0x44; 32], + }; + let order_pda = find_order_pda(&settlement, &intent.uid()).0; + + let begin = settlement_client::instructions::BeginSettle { + program_id: settlement, + finalize_ix_index: 1, + auction_id: 4242, + orders: &[settlement_client::instructions::InitializedIntent { + intent: &intent, + pulls: &[ + settlement_client::instructions::Pull { + destination: pubkey(27), + amount: 300, + }, + settlement_client::instructions::Pull { + destination: pubkey(28), + amount: 700, + }, + ], + }], + } + .into(); + let finalize = settlement_client::instructions::FinalizeSettle { + program_id: settlement, + begin_ix_index: 0, + orders: &[settlement_client::instructions::FinalizedIntent { + intent: &intent, + mint: pubkey(30), + amount: 1_234, + }], + } + .into(); + let tx = tx_from_instructions(solver, &[begin, finalize]); let expected_uid = OrderUid([0x55; 32]); let resolve_order = |pda: &Pubkey| {