diff --git a/Cargo.lock b/Cargo.lock index a938f51fbd..a6732e12b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4260,7 +4260,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]] @@ -9065,10 +9065,18 @@ 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" -source = "git+https://github.com/cowprotocol/solana-programs?rev=4e0bd7eaed7c237a327705425be2a4a50a8b4a3a#4e0bd7eaed7c237a327705425be2a4a50a8b4a3a" +source = "git+https://github.com/cowprotocol/solana-programs?rev=4d4fc46f594e0159246afaf148f0af2a463d0095#4d4fc46f594e0159246afaf148f0af2a463d0095" dependencies = [ "arrayref", "derive_more 1.0.0", @@ -9934,6 +9942,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 b73c8022f9..54922f8fd4 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" + rev = "4d4fc46f594e0159246afaf148f0af2a463d0095" } solana-client = { workspace = true } solana-sdk = { workspace = true } @@ -33,5 +33,12 @@ tracing = { workspace = true } yellowstone-grpc-client = { workspace = true } yellowstone-grpc-proto = { workspace = true } +[dev-dependencies] +settlement-client = { + package = "settlement-client", + git = "https://github.com/cowprotocol/solana-programs", + rev = "4d4fc46f594e0159246afaf148f0af2a463d0095" +} + [lints] workspace = true diff --git a/crates/solana-indexer/src/indexer/decoder.rs b/crates/solana-indexer/src/indexer/decoder.rs index efbecf10d3..d2705a6db4 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,17 +192,20 @@ 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) } - // 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: `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), @@ -243,62 +227,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 the intent 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, and 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 +283,108 @@ 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 { + }; + 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`. 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, - "BeginSettle body did not match the expected pull layout, skipping" + order_count, + push_count, + "order and push counts differ, skipping the settlement pair" ); continue; - }; + } - 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)?; - Some(TradeDelta { - order_uid: resolved.order_uid, - amount_withdrawn_delta: order.amount_withdrawn_delta, - amount_received_delta: PLACEHOLDER_AMOUNT_RECEIVED, - order_fulfilled: resolved.order_fulfilled, - }) - }) - .collect(); + let mut trades = Vec::with_capacity(order_count); + let mut corrupt = false; + for (order, amount_received_delta) in begin_input + .orders + .iter() + .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 { - 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 +394,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..7d20edf95f 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::{ @@ -31,8 +23,8 @@ use { bytes::Bytes, settlement_interface::{ Pubkey as InterfacePubkey, - SettlementInstruction, - data::intent::{EncodedOrderIntent, OrderIntent, OrderKind}, + data::intent::{OrderIntent, OrderKind}, + pda::order::find_order_pda, }, solana_sdk::pubkey::Pubkey, tokio::sync::mpsc::Sender, @@ -42,16 +34,38 @@ fn pubkey(n: u8) -> Pubkey { Pubkey::new_from_array([n; 32]) } -fn key_bytes(key: Pubkey) -> Vec { - key.to_bytes().to_vec() +/// 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 compiled(program_id_index: u32, accounts: Vec, data: Vec) -> CompiledInstruction { - CompiledInstruction { - program_id_index, - accounts, - data, - } +fn key_bytes(key: Pubkey) -> Vec { + key.to_bytes().to_vec() } fn inner( @@ -112,8 +126,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 { @@ -162,11 +184,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() }), @@ -197,7 +231,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![ @@ -236,7 +274,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))], @@ -267,7 +309,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 { @@ -295,21 +341,15 @@ 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 -/// 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]), @@ -321,19 +361,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![compiled(0, 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), @@ -354,56 +389,64 @@ 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. +/// 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 (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, 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)]. - let account_keys = vec![ - solver, - settlement, - pubkey(22), - pubkey(23), - pubkey(24), - order_pda, - pubkey(26), - pubkey(27), - pubkey(28), - ]; - - // BeginSettle body: finalize index 1, one order, bump 0xAA, two transfers of - // 300 and 700 (sum 1000 = the sell-side amount withdrawn). - let mut begin_data = vec![SettlementInstruction::BeginSettle.discriminator()]; - begin_data.extend_from_slice(&1u16.to_be_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()); - - // FinalizeSettle body: begin index 0. - let mut finalize_data = vec![SettlementInstruction::FinalizeSettle.discriminator()]; - finalize_data.extend_from_slice(&0u16.to_be_bytes()); - - let tx = tx_info( - account_keys, - vec![], - vec![], - 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), - ], - 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| { @@ -425,14 +468,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, }], }] 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,