Skip to main content

acta_maker_sdk/chain/vault/
served.rs

1use std::collections::{HashMap, HashSet};
2
3use base64::Engine as _;
4use base64::engine::general_purpose::STANDARD;
5use solana_sdk::{
6    instruction::{AccountMeta, Instruction},
7    message::AddressLookupTableAccount,
8    pubkey::Pubkey,
9};
10
11use super::manager::VaultManager;
12use super::price_route::PriceRoute;
13use super::trading::{
14    NO_AMOUNT_OFFSET, VaultSaleTerms, VaultStagingAsset, VaultSwapAmountOffset, VaultSwapHop,
15    VaultSwapKind, VaultSwapPlan,
16};
17use super::{VaultAccount, VaultChainError, pda};
18use crate::chain::ix::{
19    ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID,
20    derive_associated_token_address as ata,
21};
22use crate::orders::{SignerLike, verify_order_id_signature_bytes};
23use crate::types::{VaultLeg, VaultStagingStep, VaultSwapDirection, VaultSwapMode};
24use crate::ws::types::{SponsoredTxToSignData, SwapQuoteData, SwapQuoteResultData};
25
26const COMPUTE_BUDGET_PROGRAM_ID: Pubkey =
27    solana_sdk::pubkey!("ComputeBudget111111111111111111111111111111");
28const ED25519_PROGRAM_ID: Pubkey =
29    solana_sdk::pubkey!("Ed25519SigVerify111111111111111111111111111");
30const ED25519_HEADER: [u8; 16] = [
31    1, 0, 48, 0, 255, 255, 16, 0, 255, 255, 112, 0, 32, 0, 255, 255,
32];
33const ED25519_DATA_LEN: usize = 144;
34const SIGNATURE_LEN: usize = 64;
35const MAX_TRANSACTION_WIRE_BYTES: usize = 1232;
36const SET_COMPUTE_UNIT_LIMIT: u8 = 2;
37const SET_COMPUTE_UNIT_PRICE: u8 = 3;
38const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000;
39const BPS: u128 = 10_000;
40const STAGING_GENERATION_OFFSET: usize = 27;
41/// SPL and Sanctum multi-validator stake-pool programs.
42pub(super) const STAKE_POOL_PROGRAM_IDS: [Pubkey; 2] = [
43    solana_sdk::pubkey!("SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy"),
44    solana_sdk::pubkey!("SPMBzsVUuoHA4Jm6KunbsotaahvVikZs1JyTW6iJvbn"),
45];
46const UPDATE_STAKE_POOL_BALANCE: u8 = 7;
47
48#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
49#[non_exhaustive]
50pub enum VaultIntentError {
51    #[error("transaction is not valid base64")]
52    InvalidBase64,
53    #[error("transaction bytes are malformed: {0}")]
54    Malformed(&'static str),
55    #[error("unsupported transaction message version {0}")]
56    UnsupportedVersion(u8),
57    #[error("vault opens must use a version-0 transaction")]
58    OpenRequiresVersionZero,
59    #[error("vault opens must use an address lookup table")]
60    OpenRequiresLookupTable,
61    #[error("vault-open account {0} must be a static transaction key")]
62    OpenRequiresStaticAccount(Pubkey),
63    #[error("vault-open transaction is {actual} bytes, exceeding the {maximum}-byte wire limit")]
64    OpenTransactionTooLarge { actual: usize, maximum: usize },
65    #[error("lookup table {0} was not provided")]
66    MissingLookupTable(Pubkey),
67    #[error("lookup table {table} has no index {index}")]
68    LookupIndex { table: Pubkey, index: u8 },
69    #[error("fee payer is {found}, expected {expected}")]
70    FeePayer { expected: Pubkey, found: Pubkey },
71    #[error("transaction has {found} instructions, expected {expected}")]
72    InstructionCount { expected: usize, found: usize },
73    #[error("instruction {index} calls program {found}, expected {expected}")]
74    Program {
75        index: usize,
76        expected: Pubkey,
77        found: Pubkey,
78    },
79    #[error("instruction {index} has {found} accounts, expected {expected}")]
80    AccountCount {
81        index: usize,
82        expected: usize,
83        found: usize,
84    },
85    #[error(
86        "instruction {index} account {position} is {found} (signer {found_signer}, writable {found_writable}), expected {expected} (signer {expected_signer}, writable {expected_writable})"
87    )]
88    Account {
89        index: usize,
90        position: usize,
91        expected: Pubkey,
92        expected_signer: bool,
93        expected_writable: bool,
94        found: Pubkey,
95        found_signer: bool,
96        found_writable: bool,
97    },
98    #[error("instruction {index} data differs at byte {offset}")]
99    Data { index: usize, offset: usize },
100    #[error("account {key} is writable {found_writable}, expected {expected_writable}")]
101    Privilege {
102        key: Pubkey,
103        expected_writable: bool,
104        found_writable: bool,
105    },
106    #[error("signer {0} is not one of the transaction's required signers")]
107    SignerNotRequired(Pubkey),
108    #[error("vault-open participant signer is {found}, expected {expected}")]
109    ParticipantSigner { expected: Pubkey, found: Pubkey },
110    #[error("transaction requires signer {0} outside the intent")]
111    UnexpectedSigner(Pubkey),
112    #[error("transaction loads account {0} that no expected instruction uses")]
113    UnexpectedAccount(Pubkey),
114    #[error("compute unit limit {limit} exceeds {max}")]
115    ComputeUnitLimit { limit: u32, max: u32 },
116    #[error("compute unit price {price} exceeds {max}")]
117    ComputeUnitPrice { price: u64, max: u64 },
118    #[error("compute budget instruction {0} is not a canonical limit/price instruction")]
119    ComputeBudget(usize),
120    #[error("maker quote signature instruction: {0}")]
121    QuoteSignature(&'static str),
122    #[error("served order id differs from the one the terms bind")]
123    OrderId,
124    #[error("swap route: {0}")]
125    Route(&'static str),
126    #[error("swap terms: {0}")]
127    Terms(&'static str),
128    #[error("served {0} price section is not the vault's price route on a configured shard")]
129    PriceSection(&'static str),
130    #[error("swap setup: {0}")]
131    Setup(&'static str),
132    #[error(transparent)]
133    Intent(#[from] VaultChainError),
134}
135
136/// Ceilings for the server-chosen compute budget.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub struct ComputeBudgetBounds {
139    pub max_unit_limit: u32,
140    pub max_unit_price_micro_lamports: u64,
141}
142
143/// What the signer agreed to open; the expected transaction is rebuilt from it.
144#[derive(Debug, Clone, Copy)]
145pub enum VaultOpenIntent<'a> {
146    /// The vault writes and sells to an external maker
147    /// (`OpenPositionForVaultWriter` or `OpenSpreadForVaultWriter`, holder byte 0).
148    Sale {
149        /// The deployment's checked `GlobalConfig.hot_authority`; it pays and signs.
150        keeper: Pubkey,
151        writer: &'a VaultManager,
152        maker_owner: Pubkey,
153        maker_quote_signer: Pubkey,
154        terms: VaultSaleTerms,
155    },
156    /// The vault writes against another vault's quote
157    /// (`OpenPositionBetweenVaults` or `OpenSpreadBetweenVaults`).
158    BetweenVaults {
159        /// The deployment's checked `GlobalConfig.hot_authority`; it pays and signs.
160        keeper: Pubkey,
161        writer: &'a VaultManager,
162        holder: &'a VaultManager,
163        terms: VaultSaleTerms,
164    },
165    /// An external taker writes against the vault's quote
166    /// (`OpenPositionForVaultHolder` or `OpenSpreadForVaultHolder`).
167    Fill {
168        /// The deployment's checked `GlobalConfig.hot_authority`; it pays and signs.
169        keeper: Pubkey,
170        holder: &'a VaultManager,
171        taker: Pubkey,
172        terms: VaultSaleTerms,
173    },
174    /// A wallet buys what the vault writes; the wallet signs and pays, the vault delegate
175    /// signed the quote (`OpenPositionForVaultWriter` or `OpenSpreadForVaultWriter`, holder
176    /// byte 1). Checked by the buying wallet, never by the vault.
177    Buy {
178        /// The deployment's checked `GlobalConfig.hot_authority`; it pays and signs.
179        keeper: Pubkey,
180        writer: &'a VaultManager,
181        holder: Pubkey,
182        terms: VaultSaleTerms,
183    },
184}
185
186/// The vault pair's price routes and the push-oracle shards the deployment posts Pyth feeds to.
187#[derive(Debug, Clone, Copy)]
188pub struct PairPriceRoutes<'a> {
189    pub main: &'a PriceRoute,
190    pub second: &'a PriceRoute,
191    /// Routes of the mints the operator accepts as a staging mint; empty refuses staged swaps.
192    pub staging: &'a [PriceRoute],
193    pub shards: &'a [u16],
194}
195
196/// A swap the operator requested; the served price section must follow `price_routes`.
197#[derive(Debug, Clone, Copy)]
198pub struct VaultSwapIntent<'a> {
199    pub vault: &'a VaultManager,
200    pub request: &'a SwapQuoteData,
201    pub quote: &'a SwapQuoteResultData,
202    pub price_routes: PairPriceRoutes<'a>,
203    pub min_output_floor: u64,
204    /// The vault before the request; checked against its second leg and staging slot.
205    pub account: VaultAccount,
206}
207
208/// Verifies a served position-open transaction against `intent` and the order id it is served for.
209pub fn verify_vault_open(
210    to_sign: &SponsoredTxToSignData,
211    intent: &VaultOpenIntent<'_>,
212    tables: &[AddressLookupTableAccount],
213    bounds: ComputeBudgetBounds,
214) -> Result<VerifiedTransaction, VaultIntentError> {
215    let served = ServedMessage::decode(&to_sign.tx_base64, tables)?;
216    served.require_vault_open_wire_shape()?;
217    let (keeper, participant, quote_signer, open) = match *intent {
218        VaultOpenIntent::Sale {
219            keeper,
220            writer,
221            maker_owner,
222            maker_quote_signer,
223            terms,
224        } => (
225            keeper,
226            writer.args.actor,
227            maker_quote_signer,
228            match terms {
229                VaultSaleTerms::Physical(terms) => {
230                    writer.open_position_as_writer(keeper, maker_owner, &terms)?
231                }
232                VaultSaleTerms::Spread(terms) => {
233                    writer.open_spread_as_writer(keeper, maker_owner, &terms)?
234                }
235            },
236        ),
237        VaultOpenIntent::BetweenVaults {
238            keeper,
239            writer,
240            holder,
241            terms,
242        } => (
243            keeper,
244            writer.args.actor,
245            holder.args.delegate_authority,
246            match terms {
247                VaultSaleTerms::Physical(terms) => {
248                    writer.open_position_with_vault_holder(keeper, holder, &terms)?
249                }
250                VaultSaleTerms::Spread(terms) => {
251                    writer.open_spread_with_vault_holder(keeper, holder, &terms)?
252                }
253            },
254        ),
255        VaultOpenIntent::Fill {
256            keeper,
257            holder,
258            taker,
259            terms,
260        } => (
261            keeper,
262            taker,
263            holder.args.delegate_authority,
264            match terms {
265                VaultSaleTerms::Physical(terms) => {
266                    holder.open_position_as_holder(keeper, taker, &terms)?
267                }
268                VaultSaleTerms::Spread(terms) => {
269                    holder.open_spread_as_holder(keeper, taker, &terms)?
270                }
271            },
272        ),
273        VaultOpenIntent::Buy {
274            keeper,
275            writer,
276            holder,
277            terms,
278        } => (
279            keeper,
280            holder,
281            writer.args.delegate_authority,
282            match terms {
283                VaultSaleTerms::Physical(terms) => {
284                    writer.open_position_for_wallet_holder(keeper, holder, &terms)?
285                }
286                VaultSaleTerms::Spread(terms) => {
287                    writer.open_spread_for_wallet_holder(keeper, holder, &terms)?
288                }
289            },
290        ),
291    };
292    if open.order_id != *to_sign.order_id.as_bytes() {
293        return Err(VaultIntentError::OrderId);
294    }
295    let mut static_accounts = vec![
296        keeper,
297        participant,
298        open.addresses.position,
299        open.addresses.collateral_ata,
300    ];
301    let collateral_ata = create_ata_idempotent(
302        keeper,
303        open.addresses.position,
304        open.addresses.collateral_mint,
305        open.addresses.collateral_token_program,
306    );
307    let mut expected = served.compute_budget(bounds)?;
308    if let VaultOpenIntent::Fill {
309        holder,
310        taker,
311        terms,
312        ..
313    } = intent
314    {
315        let quote_mint = match terms {
316            VaultSaleTerms::Physical(terms) => terms.quote_mint,
317            VaultSaleTerms::Spread(terms) => terms.quote_mint,
318        };
319        let (quote_mint, quote_program) = if holder.args.main_mint == quote_mint {
320            (holder.args.main_mint, holder.args.main_token_program)
321        } else {
322            (holder.args.second_mint, holder.args.second_token_program)
323        };
324        expected.push(create_ata_idempotent(
325            keeper,
326            *taker,
327            quote_mint,
328            quote_program,
329        ));
330    }
331    expected.push(collateral_ata);
332    expected.push(served.quote_signature(expected.len(), quote_signer, &open.order_id)?);
333    expected.push(open.instruction);
334    static_accounts.extend(expected.iter().map(|instruction| instruction.program_id));
335    static_accounts.extend(
336        expected
337            .iter()
338            .flat_map(|instruction| instruction.accounts.iter())
339            .filter(|account| account.is_signer)
340            .map(|account| account.pubkey),
341    );
342    served.require_static_accounts(&static_accounts)?;
343    served.matches(keeper, &expected)
344}
345
346/// Verifies a served vault swap against the operator's request and the quoted terms.
347pub fn verify_vault_swap(
348    intent: &VaultSwapIntent<'_>,
349    tables: &[AddressLookupTableAccount],
350    bounds: ComputeBudgetBounds,
351) -> Result<VerifiedTransaction, VaultIntentError> {
352    let VaultSwapIntent {
353        vault,
354        request,
355        quote,
356        min_output_floor,
357        ..
358    } = *intent;
359    vault.ensure_current(&intent.account)?;
360    let slot = intent.account.staging;
361    if request.vault_pda != vault.addresses.vault.into() {
362        return Err(VaultIntentError::Terms("request names a different vault"));
363    }
364    if quote.request_id != request.request_id || quote.mode != request.mode {
365        return Err(VaultIntentError::Terms(
366            "quote answers a different request or mode",
367        ));
368    }
369    let kind = match (request.mode, request.direction) {
370        (VaultSwapMode::Rebalance, VaultSwapDirection::MainToSecond) => VaultSwapKind::Rebalance {
371            second_to_main: false,
372        },
373        (VaultSwapMode::Rebalance, VaultSwapDirection::SecondToMain) => VaultSwapKind::Rebalance {
374            second_to_main: true,
375        },
376        (VaultSwapMode::Recovery, VaultSwapDirection::SecondToMain) => VaultSwapKind::Recovery,
377        (VaultSwapMode::Closing, VaultSwapDirection::SecondToMain) => VaultSwapKind::Closing,
378        _ => {
379            return Err(VaultIntentError::Terms(
380                "unsupported swap mode or direction",
381            ));
382        }
383    };
384    let amount_in = parse_amount(&quote.in_amount, "quoted input")?;
385    let open_slot = slot.open;
386    match (quote.staging, open_slot) {
387        (Some(VaultStagingStep::Unstage), Some(slot)) if amount_in != slot.amount => {
388            return Err(VaultIntentError::Terms(
389                "an unstage must spend the whole staged amount",
390            ));
391        }
392        (Some(VaultStagingStep::Unstage), None) => {
393            return Err(VaultIntentError::Terms("nothing is staged to unstage"));
394        }
395        (Some(VaultStagingStep::Stage), Some(_)) => {
396            return Err(VaultIntentError::Terms(
397                "a stage needs an empty staging slot",
398            ));
399        }
400        (Some(VaultStagingStep::Unstage), Some(_)) => {}
401        _ if amount_in != parse_amount(&request.amount, "request amount")? => {
402            return Err(VaultIntentError::Terms("quoted input differs from request"));
403        }
404        _ => {}
405    }
406    let quoted_out = parse_amount(&quote.quoted_out_amount, "quoted output")?;
407    let minimum_out = parse_amount(&quote.minimum_out_amount, "minimum output")?;
408    let slippage = u128::from(request.slippage_bps);
409    if slippage > BPS
410        || quoted_out == 0
411        || minimum_out == 0
412        || u128::from(minimum_out) < u128::from(quoted_out) * (BPS - slippage) / BPS
413    {
414        return Err(VaultIntentError::Terms(
415            "minimum output exceeds the requested slippage",
416        ));
417    }
418    // A stage fills the staging mint, not the output leg the floor is in.
419    if quote.staging != Some(VaultStagingStep::Stage) && minimum_out < min_output_floor {
420        return Err(VaultIntentError::Terms(
421            "minimum output is below the operator's floor",
422        ));
423    }
424    let valid_until = parse_amount(&quote.valid_until, "valid_until")?;
425
426    let served = ServedMessage::decode(&quote.tx_base64, tables)?;
427    let payer = vault.args.actor;
428    let mut expected = served.compute_budget(bounds)?;
429    while let Some(setup) = served
430        .instructions
431        .get(expected.len())
432        .filter(|ix| ix.program_id != vault.args.program_id)
433    {
434        expected.push(swap_setup(vault, intent.price_routes, payer, setup)?);
435    }
436    let index = expected.len();
437    let swap = served
438        .instructions
439        .get(index)
440        .ok_or(VaultIntentError::InstructionCount {
441            expected: index + 1,
442            found: served.instructions.len(),
443        })?;
444    let routes = intent.price_routes;
445    if routes.main.mint != vault.args.main_mint || routes.second.mint != vault.args.second_mint {
446        return Err(VaultIntentError::Intent(VaultChainError::InvalidInput(
447            "price routes must price the vault's main and second mints",
448        )));
449    }
450    let a = &vault.addresses;
451    let mut priced_legs = [
452        ("main", a.main_price_route, routes.main),
453        ("second", a.second_price_route, routes.second),
454    ];
455    let mut kind = match quote.staging {
456        None => kind,
457        Some(step) => {
458            let (Some(mint), Some(token_program)) = (swap.accounts.get(9), swap.accounts.get(10))
459            else {
460                return Err(VaultIntentError::Route(
461                    "swap instruction is shorter than its fixed layout",
462                ));
463            };
464            let route = routes
465                .staging
466                .iter()
467                .find(|route| route.mint == mint.pubkey)
468                .ok_or(VaultIntentError::PriceSection("staging"))?;
469            let expected = match (step, open_slot) {
470                (VaultStagingStep::Unstage, Some(slot)) if slot.mint != mint.pubkey => {
471                    return Err(VaultIntentError::Terms(
472                        "unstage names another staging mint",
473                    ));
474                }
475                (VaultStagingStep::Unstage, _) => Some(slot.generation),
476                _ => slot.generation.checked_add(1),
477            };
478            let served = swap
479                .data
480                .get(STAGING_GENERATION_OFFSET..STAGING_GENERATION_OFFSET + 8)
481                .and_then(|bytes| <[u8; 8]>::try_from(bytes).ok())
482                .map(u64::from_le_bytes);
483            let generation = match (expected, served) {
484                (Some(expected), Some(served)) if expected == served => expected,
485                _ => {
486                    return Err(VaultIntentError::Terms(
487                        "staging generation does not name the vault's slot",
488                    ));
489                }
490            };
491            // Stage leaves the input leg, unstage enters the output leg.
492            let leg = match (step, request.direction) {
493                (VaultStagingStep::Stage, VaultSwapDirection::MainToSecond)
494                | (VaultStagingStep::Unstage, VaultSwapDirection::SecondToMain) => VaultLeg::Main,
495                _ => VaultLeg::Second,
496            };
497            priced_legs = [
498                priced_legs[usize::from(leg == VaultLeg::Second)],
499                (
500                    "staging",
501                    pda::price_route(&vault.args.program_id, &route.mint).0,
502                    route,
503                ),
504            ];
505            VaultSwapKind::Staging {
506                step,
507                leg,
508                asset: VaultStagingAsset {
509                    mint: mint.pubkey,
510                    token_program: token_program.pubkey,
511                },
512                generation,
513            }
514        }
515    };
516    let (fixed, header) = (kind.fixed_accounts(), kind.header_len());
517    let (Some(priced), Some(hop_data)) = (swap.accounts.get(fixed..), swap.data.get(header..))
518    else {
519        return Err(VaultIntentError::Route(
520            "swap instruction is shorter than its fixed layout",
521        ));
522    };
523    let price_section = served_price_section(priced_legs, routes.shards, priced)?;
524    let mut hop_accounts = &priced[price_section.len()..];
525    let mut recovery_tail_len = 0;
526    if request.mode == VaultSwapMode::Recovery
527        && let VaultSwapKind::Staging {
528            step: VaultStagingStep::Stage,
529            asset,
530            generation,
531            ..
532        } = kind
533    {
534        let tail = hop_accounts
535            .get(2..)
536            .ok_or(VaultIntentError::Route("recovery tail is truncated"))?;
537        let main_price_section = served_price_section(
538            [("main", a.main_price_route, routes.main)],
539            routes.shards,
540            tail,
541        )?;
542        recovery_tail_len = 2 + main_price_section.len();
543        hop_accounts = &hop_accounts[recovery_tail_len..];
544        kind = VaultSwapKind::RecoveryStage {
545            asset,
546            generation,
547            main_price_section,
548        };
549    }
550    if hop_accounts.iter().any(|meta| meta.pubkey == payer) {
551        return Err(VaultIntentError::Route("route uses the fee payer"));
552    }
553    let share_mint = vault.args.share_mint;
554    let share_ata = ata(&vault.addresses.vault, &share_mint, &TOKEN_PROGRAM_ID);
555    if hop_accounts
556        .iter()
557        .any(|meta| meta.pubkey == share_mint || meta.pubkey == share_ata)
558    {
559        return Err(VaultIntentError::Route(
560            "route touches the vault share mint or its share account",
561        ));
562    }
563    let plan = VaultSwapPlan {
564        price_section,
565        amount_in,
566        minimum_out,
567        valid_until,
568        hops: served_hops(swap.data[header - 1], hop_data, hop_accounts)?,
569    };
570    let instruction = vault
571        .swap(kind.clone(), &plan)
572        .map_err(|error| match error {
573            VaultChainError::InvalidInput(reason) => VaultIntentError::Route(reason),
574            other => VaultIntentError::Intent(other),
575        })?;
576    // Hops may write price or mint accounts: prices are read first, the vault is no mint authority.
577    let staging = match kind {
578        VaultSwapKind::Staging { asset, .. } | VaultSwapKind::RecoveryStage { asset, .. } => {
579            Some(asset.mint)
580        }
581        _ => None,
582    };
583    let mint = |key: &Pubkey| {
584        *key == vault.args.main_mint || *key == vault.args.second_mint || Some(*key) == staging
585    };
586    let mut pinned: Vec<&AccountMeta> = instruction.accounts[..fixed]
587        .iter()
588        .filter(|meta| !mint(&meta.pubkey))
589        .collect();
590    let mut position = fixed + plan.price_section.len();
591    pinned.extend(&instruction.accounts[position..position + recovery_tail_len]);
592    position += recovery_tail_len;
593    for hop in &plan.hops {
594        pinned.extend(&instruction.accounts[position..position + 3]);
595        position += 3 + hop.accounts.len();
596    }
597    if hop_accounts.iter().any(|meta| {
598        meta.is_writable
599            && pinned
600                .iter()
601                .any(|fixed| fixed.pubkey == meta.pubkey && !fixed.is_writable)
602    }) {
603        return Err(VaultIntentError::Route(
604            "route makes a read-only account writable",
605        ));
606    }
607    require_route_atas(&expected, &plan.hops)?;
608    expected.push(instruction);
609    served.matches(payer, &expected)
610}
611
612/// Rebuilds served setup: transit/scratch ATAs or `UpdateStakePoolBalance` of a pinned pool.
613fn swap_setup(
614    vault: &VaultManager,
615    routes: PairPriceRoutes<'_>,
616    payer: Pubkey,
617    served: &Instruction,
618) -> Result<Instruction, VaultIntentError> {
619    let key = |position: usize| {
620        served
621            .accounts
622            .get(position)
623            .map(|meta| meta.pubkey)
624            .ok_or(VaultIntentError::Setup("instruction is truncated"))
625    };
626    let token_program = |position: usize| {
627        key(position).and_then(|program| {
628            if program == TOKEN_PROGRAM_ID || program == TOKEN_2022_PROGRAM_ID {
629                Ok(program)
630            } else {
631                Err(VaultIntentError::Setup("token program is not SPL Token"))
632            }
633        })
634    };
635    if served.program_id == ASSOCIATED_TOKEN_PROGRAM_ID {
636        let mint = key(3)?;
637        let args = &vault.args;
638        if [args.main_mint, args.second_mint, args.share_mint].contains(&mint) {
639            return Err(VaultIntentError::Setup(
640                "creates an account for a vault leg or share mint",
641            ));
642        }
643        return Ok(create_ata_idempotent(
644            payer,
645            vault.addresses.vault,
646            mint,
647            token_program(5)?,
648        ));
649    }
650    if !STAKE_POOL_PROGRAM_IDS.contains(&served.program_id) {
651        return Err(VaultIntentError::Setup(
652            "unexpected instruction before the swap",
653        ));
654    }
655    let pool = key(0)?;
656    let pinned = [routes.main, routes.second]
657        .into_iter()
658        .chain(routes.staging)
659        .flat_map(PriceRoute::account_ratios)
660        .any(|ratio| ratio.account == pool);
661    if !pinned {
662        return Err(VaultIntentError::Setup(
663            "updates a stake pool the vault's routes do not pin",
664        ));
665    }
666    Ok(update_stake_pool_balance(
667        served.program_id,
668        [pool, key(2)?, key(3)?, key(4)?, key(5)?, token_program(6)?],
669    ))
670}
671
672pub(super) fn update_stake_pool_balance(program: Pubkey, keys: [Pubkey; 6]) -> Instruction {
673    let [pool, validators, reserve, fee, mint, token_program] = keys;
674    let authority = Pubkey::find_program_address(&[pool.as_ref(), b"withdraw"], &program).0;
675    Instruction {
676        program_id: program,
677        accounts: vec![
678            AccountMeta::new(pool, false),
679            AccountMeta::new_readonly(authority, false),
680            AccountMeta::new(validators, false),
681            AccountMeta::new_readonly(reserve, false),
682            AccountMeta::new(fee, false),
683            AccountMeta::new(mint, false),
684            AccountMeta::new_readonly(token_program, false),
685        ],
686        data: vec![UPDATE_STAKE_POOL_BALANCE],
687    }
688}
689
690/// Lookup tables a served transaction references, to fetch before verification.
691pub fn served_lookup_table_keys(tx_base64: &str) -> Result<Vec<Pubkey>, VaultIntentError> {
692    let bytes = STANDARD
693        .decode(tx_base64)
694        .map_err(|_| VaultIntentError::InvalidBase64)?;
695    Ok(CompiledMessage::decode(&bytes)?
696        .lookups
697        .into_iter()
698        .map(|lookup| lookup.table)
699        .collect())
700}
701
702/// Each leg must be its route followed by its edge accounts on one configured shard.
703fn served_price_section<const N: usize>(
704    legs: [(&'static str, Pubkey, &PriceRoute); N],
705    shards: &[u16],
706    served: &[AccountMeta],
707) -> Result<Vec<AccountMeta>, VaultIntentError> {
708    let mut section = Vec::new();
709    for (leg, address, route) in legs {
710        let rest = &served[section.len()..];
711        let found = shards
712            .iter()
713            .map(|shard| route.accounts(address, *shard))
714            .find(|expected| {
715                rest.get(..expected.len()).is_some_and(|served| {
716                    served
717                        .iter()
718                        .zip(expected)
719                        .all(|(served, expected)| served.pubkey == expected.pubkey)
720                })
721            })
722            .ok_or(VaultIntentError::PriceSection(leg))?;
723        section.extend(found);
724    }
725    Ok(section)
726}
727
728/// Splits the served hops back out; the rebuilt instruction must match byte for byte.
729fn require_route_atas(
730    setup: &[Instruction],
731    hops: &[VaultSwapHop],
732) -> Result<(), VaultIntentError> {
733    let transit = hops.split_last().map_or(&[][..], |(_, transit)| transit);
734    let mut created = Vec::new();
735    for ix in setup
736        .iter()
737        .filter(|ix| ix.program_id == ASSOCIATED_TOKEN_PROGRAM_ID)
738    {
739        let ata = ix.accounts[1].pubkey;
740        let routed = transit.iter().any(|hop| hop.destination == ata)
741            || hops
742                .iter()
743                .any(|hop| hop.accounts.iter().any(|meta| meta.pubkey == ata));
744        if !routed {
745            return Err(VaultIntentError::Setup(
746                "creates an account the route does not use",
747            ));
748        }
749        if created.contains(&ata) {
750            return Err(VaultIntentError::Setup("creates the same account twice"));
751        }
752        created.push(ata);
753    }
754    Ok(())
755}
756
757fn served_hops(
758    count: u8,
759    mut data: &[u8],
760    mut accounts: &[AccountMeta],
761) -> Result<Vec<VaultSwapHop>, VaultIntentError> {
762    let truncated = VaultIntentError::Route("swap hops are truncated");
763    let mut hops = Vec::with_capacity(usize::from(count));
764    for _ in 0..count {
765        let [account_count, lo, hi, offset_lo, offset_hi, rest @ ..] = data else {
766            return Err(truncated);
767        };
768        let (amount_offset, rest) = match u16::from_le_bytes([*offset_lo, *offset_hi]) {
769            NO_AMOUNT_OFFSET => (None, rest),
770            offset => {
771                let Some((num, rest)) = rest.split_first_chunk::<8>() else {
772                    return Err(truncated);
773                };
774                let Some((den, rest)) = rest.split_first_chunk::<8>() else {
775                    return Err(truncated);
776                };
777                let amount = VaultSwapAmountOffset {
778                    offset,
779                    num: u64::from_le_bytes(*num),
780                    den: u64::from_le_bytes(*den),
781                };
782                (Some(amount), rest)
783            }
784        };
785        let Some((hop_data, rest)) =
786            rest.split_at_checked(usize::from(u16::from_le_bytes([*lo, *hi])))
787        else {
788            return Err(truncated);
789        };
790        let [program, source, destination, tail @ ..] = accounts else {
791            return Err(truncated);
792        };
793        let Some((cpi, tail)) = tail.split_at_checked(usize::from(*account_count)) else {
794            return Err(truncated);
795        };
796        hops.push(VaultSwapHop {
797            program: program.pubkey,
798            source: source.pubkey,
799            destination: destination.pubkey,
800            accounts: cpi.to_vec(),
801            data: hop_data.to_vec(),
802            amount_offset,
803        });
804        data = rest;
805        accounts = tail;
806    }
807    Ok(hops)
808}
809
810fn parse_amount(value: &str, field: &'static str) -> Result<u64, VaultIntentError> {
811    value
812        .trim()
813        .parse::<u64>()
814        .map_err(|_| VaultIntentError::Terms(field))
815}
816
817pub(super) fn create_ata_idempotent(
818    payer: Pubkey,
819    owner: Pubkey,
820    mint: Pubkey,
821    token_program: Pubkey,
822) -> Instruction {
823    Instruction {
824        program_id: ASSOCIATED_TOKEN_PROGRAM_ID,
825        accounts: vec![
826            AccountMeta::new(payer, true),
827            AccountMeta::new(
828                crate::chain::ix::derive_associated_token_address(&owner, &mint, &token_program),
829                false,
830            ),
831            AccountMeta::new_readonly(owner, false),
832            AccountMeta::new_readonly(mint, false),
833            AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
834            AccountMeta::new_readonly(token_program, false),
835        ],
836        data: vec![1],
837    }
838}
839
840struct Lookup {
841    table: Pubkey,
842    writable: Vec<u8>,
843    readonly: Vec<u8>,
844}
845
846struct CompiledInstruction {
847    program: u8,
848    accounts: Vec<u8>,
849    data: Vec<u8>,
850}
851
852struct CompiledMessage {
853    signatures_start: usize,
854    message_start: usize,
855    required_signers: usize,
856    readonly_signed: usize,
857    readonly_unsigned: usize,
858    static_keys: Vec<Pubkey>,
859    instructions: Vec<CompiledInstruction>,
860    lookups: Vec<Lookup>,
861    is_version_zero: bool,
862}
863
864struct Reader<'a> {
865    bytes: &'a [u8],
866    pos: usize,
867}
868
869impl<'a> Reader<'a> {
870    fn take(&mut self, len: usize) -> Result<&'a [u8], VaultIntentError> {
871        let end = self
872            .pos
873            .checked_add(len)
874            .filter(|end| *end <= self.bytes.len())
875            .ok_or(VaultIntentError::Malformed("truncated transaction"))?;
876        let slice = &self.bytes[self.pos..end];
877        self.pos = end;
878        Ok(slice)
879    }
880
881    fn byte(&mut self) -> Result<u8, VaultIntentError> {
882        Ok(self.take(1)?[0])
883    }
884
885    fn compact(&mut self) -> Result<usize, VaultIntentError> {
886        let mut value = 0usize;
887        for shift in [0u32, 7, 14] {
888            let byte = self.byte()?;
889            value |= usize::from(byte & 0x7f) << shift;
890            if byte & 0x80 == 0 {
891                return Ok(value);
892            }
893        }
894        Err(VaultIntentError::Malformed("length prefix too long"))
895    }
896
897    fn key(&mut self) -> Result<Pubkey, VaultIntentError> {
898        let mut bytes = [0; 32];
899        bytes.copy_from_slice(self.take(32)?);
900        Ok(Pubkey::new_from_array(bytes))
901    }
902
903    fn vec(&mut self) -> Result<Vec<u8>, VaultIntentError> {
904        let len = self.compact()?;
905        Ok(self.take(len)?.to_vec())
906    }
907}
908
909impl CompiledMessage {
910    fn decode(transaction: &[u8]) -> Result<Self, VaultIntentError> {
911        let mut reader = Reader {
912            bytes: transaction,
913            pos: 0,
914        };
915        let signatures = reader.compact()?;
916        let signatures_start = reader.pos;
917        reader.take(signatures.saturating_mul(SIGNATURE_LEN))?;
918        let message_start = reader.pos;
919        let mut versioned = false;
920        if reader
921            .bytes
922            .get(reader.pos)
923            .is_some_and(|byte| byte & 0x80 != 0)
924        {
925            let version = reader.byte()? & 0x7f;
926            if version != 0 {
927                return Err(VaultIntentError::UnsupportedVersion(version));
928            }
929            versioned = true;
930        }
931        let required_signers = usize::from(reader.byte()?);
932        let readonly_signed = usize::from(reader.byte()?);
933        let readonly_unsigned = usize::from(reader.byte()?);
934        let key_count = reader.compact()?;
935        let static_keys = (0..key_count)
936            .map(|_| reader.key())
937            .collect::<Result<Vec<_>, _>>()?;
938        if signatures != required_signers
939            || readonly_signed > required_signers
940            || required_signers == readonly_signed
941            || required_signers + readonly_unsigned > static_keys.len()
942        {
943            return Err(VaultIntentError::Malformed("invalid message header"));
944        }
945        reader.take(32)?;
946        let instruction_count = reader.compact()?;
947        let instructions = (0..instruction_count)
948            .map(|_| {
949                Ok(CompiledInstruction {
950                    program: reader.byte()?,
951                    accounts: reader.vec()?,
952                    data: reader.vec()?,
953                })
954            })
955            .collect::<Result<Vec<_>, VaultIntentError>>()?;
956        let mut lookups = Vec::new();
957        if versioned {
958            for _ in 0..reader.compact()? {
959                lookups.push(Lookup {
960                    table: reader.key()?,
961                    writable: reader.vec()?,
962                    readonly: reader.vec()?,
963                });
964            }
965        }
966        if reader.pos != transaction.len() {
967            return Err(VaultIntentError::Malformed("trailing bytes"));
968        }
969        Ok(Self {
970            signatures_start,
971            message_start,
972            required_signers,
973            readonly_signed,
974            readonly_unsigned,
975            static_keys,
976            instructions,
977            lookups,
978            is_version_zero: versioned,
979        })
980    }
981}
982
983#[derive(Clone, Copy, PartialEq, Eq)]
984struct Flags {
985    signer: bool,
986    writable: bool,
987}
988
989struct ServedMessage {
990    keys: Vec<(Pubkey, Flags)>,
991    instructions: Vec<Instruction>,
992    transaction: VerifiedTransaction,
993    static_keys: HashSet<Pubkey>,
994    is_version_zero: bool,
995    has_lookup_table: bool,
996    wire_len: usize,
997}
998
999#[derive(Debug)]
1000pub struct VerifiedTransaction {
1001    bytes: Vec<u8>,
1002    signatures_start: usize,
1003    message_start: usize,
1004    signers: Vec<Pubkey>,
1005}
1006
1007impl VerifiedTransaction {
1008    pub fn sign(self, signer: &dyn SignerLike) -> Result<String, VaultIntentError> {
1009        self.sign_with_participant_signature(signer)
1010            .map(|(transaction, _)| transaction)
1011    }
1012
1013    pub(crate) fn sign_with_participant_signature(
1014        mut self,
1015        signer: &dyn SignerLike,
1016    ) -> Result<(String, String), VaultIntentError> {
1017        let key = Pubkey::new_from_array(signer.pubkey_bytes());
1018        let index = self
1019            .signers
1020            .iter()
1021            .position(|required| *required == key)
1022            .ok_or(VaultIntentError::SignerNotRequired(key))?;
1023        let signature = signer.sign_message(&self.bytes[self.message_start..]);
1024        let slot = self.signatures_start + index * SIGNATURE_LEN;
1025        self.bytes[slot..slot + SIGNATURE_LEN].copy_from_slice(&signature);
1026        Ok((
1027            STANDARD.encode(self.bytes),
1028            crate::wire::encode_base58(&signature),
1029        ))
1030    }
1031}
1032
1033impl ServedMessage {
1034    fn decode(
1035        tx_base64: &str,
1036        tables: &[AddressLookupTableAccount],
1037    ) -> Result<Self, VaultIntentError> {
1038        let bytes = STANDARD
1039            .decode(tx_base64)
1040            .map_err(|_| VaultIntentError::InvalidBase64)?;
1041        let wire_len = bytes.len();
1042        let message = CompiledMessage::decode(&bytes)?;
1043        let static_len = message.static_keys.len();
1044        let mut keys: Vec<(Pubkey, Flags)> = message
1045            .static_keys
1046            .iter()
1047            .enumerate()
1048            .map(|(index, key)| {
1049                let flags = if index < message.required_signers {
1050                    Flags {
1051                        signer: true,
1052                        writable: index < message.required_signers - message.readonly_signed,
1053                    }
1054                } else {
1055                    Flags {
1056                        signer: false,
1057                        writable: index < static_len - message.readonly_unsigned,
1058                    }
1059                };
1060                (*key, flags)
1061            })
1062            .collect();
1063        for writable in [true, false] {
1064            for lookup in &message.lookups {
1065                let table = tables
1066                    .iter()
1067                    .find(|table| table.key == lookup.table)
1068                    .ok_or(VaultIntentError::MissingLookupTable(lookup.table))?;
1069                let indexes = if writable {
1070                    &lookup.writable
1071                } else {
1072                    &lookup.readonly
1073                };
1074                for index in indexes {
1075                    let key = table.addresses.get(usize::from(*index)).ok_or(
1076                        VaultIntentError::LookupIndex {
1077                            table: lookup.table,
1078                            index: *index,
1079                        },
1080                    )?;
1081                    keys.push((
1082                        *key,
1083                        Flags {
1084                            signer: false,
1085                            writable,
1086                        },
1087                    ));
1088                }
1089            }
1090        }
1091        let mut seen = HashSet::with_capacity(keys.len());
1092        if !keys.iter().all(|(key, _)| seen.insert(*key)) {
1093            return Err(VaultIntentError::Malformed("duplicate account key"));
1094        }
1095        let instructions = message
1096            .instructions
1097            .iter()
1098            .map(|compiled| {
1099                let key = |index: u8| {
1100                    keys.get(usize::from(index))
1101                        .ok_or(VaultIntentError::Malformed("account index out of range"))
1102                };
1103                let (program_id, _) = key(compiled.program)?;
1104                let accounts = compiled
1105                    .accounts
1106                    .iter()
1107                    .map(|index| {
1108                        key(*index).map(|(pubkey, flags)| AccountMeta {
1109                            pubkey: *pubkey,
1110                            is_signer: flags.signer,
1111                            is_writable: flags.writable,
1112                        })
1113                    })
1114                    .collect::<Result<Vec<_>, _>>()?;
1115                Ok(Instruction {
1116                    program_id: *program_id,
1117                    accounts,
1118                    data: compiled.data.clone(),
1119                })
1120            })
1121            .collect::<Result<Vec<_>, VaultIntentError>>()?;
1122        let transaction = VerifiedTransaction {
1123            signatures_start: message.signatures_start,
1124            message_start: message.message_start,
1125            signers: message.static_keys[..message.required_signers].to_vec(),
1126            bytes,
1127        };
1128        Ok(Self {
1129            keys,
1130            instructions,
1131            transaction,
1132            static_keys: message.static_keys.into_iter().collect(),
1133            is_version_zero: message.is_version_zero,
1134            has_lookup_table: message
1135                .lookups
1136                .iter()
1137                .any(|lookup| !lookup.writable.is_empty() || !lookup.readonly.is_empty()),
1138            wire_len,
1139        })
1140    }
1141
1142    fn require_vault_open_wire_shape(&self) -> Result<(), VaultIntentError> {
1143        if !self.is_version_zero {
1144            return Err(VaultIntentError::OpenRequiresVersionZero);
1145        }
1146        if !self.has_lookup_table {
1147            return Err(VaultIntentError::OpenRequiresLookupTable);
1148        }
1149        if self.wire_len > MAX_TRANSACTION_WIRE_BYTES {
1150            return Err(VaultIntentError::OpenTransactionTooLarge {
1151                actual: self.wire_len,
1152                maximum: MAX_TRANSACTION_WIRE_BYTES,
1153            });
1154        }
1155        Ok(())
1156    }
1157
1158    fn require_static_accounts(&self, accounts: &[Pubkey]) -> Result<(), VaultIntentError> {
1159        accounts
1160            .iter()
1161            .find(|account| !self.static_keys.contains(account))
1162            .copied()
1163            .map_or(Ok(()), |account| {
1164                Err(VaultIntentError::OpenRequiresStaticAccount(account))
1165            })
1166    }
1167
1168    fn compute_budget(
1169        &self,
1170        bounds: ComputeBudgetBounds,
1171    ) -> Result<Vec<Instruction>, VaultIntentError> {
1172        let budget = |index: usize, tag: u8, len: usize| {
1173            self.instructions
1174                .get(index)
1175                .filter(|ix| {
1176                    ix.program_id == COMPUTE_BUDGET_PROGRAM_ID
1177                        && ix.accounts.is_empty()
1178                        && ix.data.len() == len
1179                        && ix.data.first() == Some(&tag)
1180                })
1181                .map(|ix| &ix.data[1..])
1182                .ok_or(VaultIntentError::ComputeBudget(index))
1183        };
1184        let mut limit = [0; 4];
1185        limit.copy_from_slice(budget(0, SET_COMPUTE_UNIT_LIMIT, 5)?);
1186        let limit = u32::from_le_bytes(limit);
1187        let mut price = [0; 8];
1188        price.copy_from_slice(budget(1, SET_COMPUTE_UNIT_PRICE, 9)?);
1189        let price = u64::from_le_bytes(price);
1190        let max_limit = bounds.max_unit_limit.min(MAX_COMPUTE_UNIT_LIMIT);
1191        if limit == 0 || limit > max_limit {
1192            return Err(VaultIntentError::ComputeUnitLimit {
1193                limit,
1194                max: max_limit,
1195            });
1196        }
1197        if price > bounds.max_unit_price_micro_lamports {
1198            return Err(VaultIntentError::ComputeUnitPrice {
1199                price,
1200                max: bounds.max_unit_price_micro_lamports,
1201            });
1202        }
1203        let instruction = |tag: u8, value: &[u8]| {
1204            let mut data = Vec::with_capacity(1 + value.len());
1205            data.push(tag);
1206            data.extend_from_slice(value);
1207            Instruction {
1208                program_id: COMPUTE_BUDGET_PROGRAM_ID,
1209                accounts: Vec::new(),
1210                data,
1211            }
1212        };
1213        Ok(vec![
1214            instruction(SET_COMPUTE_UNIT_LIMIT, &limit.to_le_bytes()),
1215            instruction(SET_COMPUTE_UNIT_PRICE, &price.to_le_bytes()),
1216        ])
1217    }
1218
1219    fn quote_signature(
1220        &self,
1221        index: usize,
1222        signer: Pubkey,
1223        order_id: &[u8; 32],
1224    ) -> Result<Instruction, VaultIntentError> {
1225        let ix = self
1226            .instructions
1227            .get(index)
1228            .ok_or(VaultIntentError::QuoteSignature("missing"))?;
1229        if ix.program_id != ED25519_PROGRAM_ID
1230            || !ix.accounts.is_empty()
1231            || ix.data.len() != ED25519_DATA_LEN
1232            || ix.data[..16] != ED25519_HEADER
1233        {
1234            return Err(VaultIntentError::QuoteSignature(
1235                "not a single inline Ed25519 check",
1236            ));
1237        }
1238        if ix.data[16..48] != signer.to_bytes() {
1239            return Err(VaultIntentError::QuoteSignature(
1240                "binds a different quote signer",
1241            ));
1242        }
1243        if ix.data[112..] != *order_id {
1244            return Err(VaultIntentError::QuoteSignature("binds a different order"));
1245        }
1246        let mut signature = [0; SIGNATURE_LEN];
1247        signature.copy_from_slice(&ix.data[48..112]);
1248        verify_order_id_signature_bytes(order_id, &signature, &signer.to_bytes())
1249            .map_err(|_| VaultIntentError::QuoteSignature("signature does not verify"))?;
1250        Ok(ix.clone())
1251    }
1252
1253    fn matches(
1254        self,
1255        payer: Pubkey,
1256        expected: &[Instruction],
1257    ) -> Result<VerifiedTransaction, VaultIntentError> {
1258        let mut flags: HashMap<Pubkey, Flags> = HashMap::new();
1259        flags.insert(
1260            payer,
1261            Flags {
1262                signer: true,
1263                writable: true,
1264            },
1265        );
1266        for ix in expected {
1267            flags.entry(ix.program_id).or_insert(Flags {
1268                signer: false,
1269                writable: false,
1270            });
1271            for meta in &ix.accounts {
1272                let entry = flags.entry(meta.pubkey).or_insert(Flags {
1273                    signer: false,
1274                    writable: false,
1275                });
1276                entry.signer |= meta.is_signer;
1277                entry.writable |= meta.is_writable;
1278            }
1279        }
1280        if let Some((found, _)) = self.keys.first().filter(|(key, _)| *key != payer) {
1281            return Err(VaultIntentError::FeePayer {
1282                expected: payer,
1283                found: *found,
1284            });
1285        }
1286        if self.instructions.len() != expected.len() {
1287            return Err(VaultIntentError::InstructionCount {
1288                expected: expected.len(),
1289                found: self.instructions.len(),
1290            });
1291        }
1292        for (index, (served, expected)) in self.instructions.iter().zip(expected).enumerate() {
1293            if served.program_id != expected.program_id {
1294                return Err(VaultIntentError::Program {
1295                    index,
1296                    expected: expected.program_id,
1297                    found: served.program_id,
1298                });
1299            }
1300            if served.accounts.len() != expected.accounts.len() {
1301                return Err(VaultIntentError::AccountCount {
1302                    index,
1303                    expected: expected.accounts.len(),
1304                    found: served.accounts.len(),
1305                });
1306            }
1307            for (position, (found, wanted)) in
1308                served.accounts.iter().zip(&expected.accounts).enumerate()
1309            {
1310                let wanted_flags = match flags.get(&wanted.pubkey) {
1311                    Some(flags) => *flags,
1312                    None => Flags {
1313                        signer: wanted.is_signer,
1314                        writable: wanted.is_writable,
1315                    },
1316                };
1317                if found.pubkey != wanted.pubkey
1318                    || found.is_signer != wanted_flags.signer
1319                    || found.is_writable != wanted_flags.writable
1320                {
1321                    return Err(VaultIntentError::Account {
1322                        index,
1323                        position,
1324                        expected: wanted.pubkey,
1325                        expected_signer: wanted_flags.signer,
1326                        expected_writable: wanted_flags.writable,
1327                        found: found.pubkey,
1328                        found_signer: found.is_signer,
1329                        found_writable: found.is_writable,
1330                    });
1331                }
1332            }
1333            if served.data != expected.data {
1334                let offset = match served
1335                    .data
1336                    .iter()
1337                    .zip(&expected.data)
1338                    .position(|(a, b)| a != b)
1339                {
1340                    Some(offset) => offset,
1341                    None => served.data.len().min(expected.data.len()),
1342                };
1343                return Err(VaultIntentError::Data { index, offset });
1344            }
1345        }
1346        for (key, found) in &self.keys {
1347            match flags.get(key) {
1348                None => return Err(VaultIntentError::UnexpectedAccount(*key)),
1349                Some(wanted) if found.signer && !wanted.signer => {
1350                    return Err(VaultIntentError::UnexpectedSigner(*key));
1351                }
1352                Some(wanted) if found != wanted => {
1353                    return Err(VaultIntentError::Privilege {
1354                        key: *key,
1355                        expected_writable: wanted.writable,
1356                        found_writable: found.writable,
1357                    });
1358                }
1359                Some(_) => {}
1360            }
1361        }
1362        Ok(self.transaction)
1363    }
1364}
1365
1366#[cfg(test)]
1367#[path = "served_tests.rs"]
1368mod tests;