Skip to content

feat(solana-solvers): PR 3 solution assembly (custom interactions, prices, lookup tables)#4652

Open
squadgazzz wants to merge 18 commits into
mainfrom
solana-solvers/pr3-solution-assembly
Open

feat(solana-solvers): PR 3 solution assembly (custom interactions, prices, lookup tables)#4652
squadgazzz wants to merge 18 commits into
mainfrom
solana-solvers/pr3-solution-assembly

Conversation

@squadgazzz

@squadgazzz squadgazzz commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Description

PR 3 of the Jupiter solver plan (after #4632, #4635, #4637): turn one quoted swap into the single-order solution the driver consumes on /solve.

A solution is a receipt the driver can act on: which prices clear the trade, which order it fills, and the exact instructions to splice into the settlement transaction. The solver's only degrees of freedom are the clearing prices, derived from the quoted amounts so executed times price matches on both sides, and the interactions array, which carries Jupiter's instructions verbatim with full account metas. Address lookup tables ride along so the driver can build the v0 transaction the instructions assume.

Amounts go out as decimal strings, not JSON numbers. A Solana u64 reaches ~1.8e19, but a JSON number read as a double is only exact up to 2^53 (~9e15), and anything past that rounds to the nearest value the double can hold. Real amounts get there (10M SOL is 1e16 lamports), so a bare number risks silent corruption in a reader like JavaScript, whose only number type is a double. A string carries the exact digits and each consumer parses it back whole. The EVM solution DTO strings its amounts for the same reason.

Deliberately minimal:

  • cu_estimate is optional and left unset. The driver sizes the CU limit by simulating the whole settlement transaction (driver §7.2), which this solver never sees, so it has no meaningful total to report. This mirrors the EVM solution's optional gas. The adapter still drops Jupiter's own compute-budget instructions, since the driver emits its own.
  • No payout instruction. The swap output lands in the settlement's per-token buffer and FinalizeSettle pushes each order's amount to the user (Push proceeds to users in FinalizeSettle solana-programs#62).
  • No slippage handling. Jupiter bakes it into the instruction data and the driver applies none to solver-supplied interactions.
  • Interactions are a flat Vec<Instruction>, matching the driver's single interaction kind at MVP. There is no liquidity-interaction variant: the on-chain liquidity subsystem is frozen, so every interaction is a solver-supplied instruction.

Guards: same-mint orders are rejected (a uniform price map cannot hold two prices for one mint), and zero quoted amounts are rejected.

Changes

  • New domain::solution module: Solution, Trade, and a flat Instruction DTO (matching driver §2's Vec<Instruction>), wire serialization (base58 keys, hex order uid, base64 instruction data), and Solution::single mapping a dex::Swap into the DTO
  • Token amounts (prices values, executed_amount, fee) serialize as decimal strings (see the precision note above)
  • cu_estimate: Option<u64> on the solution, left None (the driver sizes the CU limit from simulation, mirroring EVM's optional gas)
  • OrderUid newtype (32-byte hash(intent)), hex-formatted the same way as the EVM model crate

How to test

New unit tests: sell and buy mapping, clearing prices, verbatim instruction carry-over, lookup-table pass-through, rejection guards, and a wire-format test pinning the JSON shape.

@squadgazzz
squadgazzz marked this pull request as ready for review July 21, 2026 08:29
@squadgazzz
squadgazzz requested a review from a team as a code owner July 21, 2026 08:29
@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @squadgazzz's task in 4m 43s —— View job


Review: PR #4652 — Solana solvers solution assembly

  • Read changed files & gather context
  • Review domain/solution.rs (core logic)
  • Review order.rs, jupiter mod/dto changes
  • Post inline findings & summary

The clearing-price math is sound: pricing each mint at the counter amount (sell→out, buy→in) makes executed × price cancel exactly on both sides for both order sides, with no rounding exposure. The SameMint/ZeroAmount guards are correct and necessary, instructions are carried through verbatim, and the OrderUid hex Display matches the model crate. The design decisions the description calls out (no CU estimate, no payout, no slippage, single Custom variant, 32-byte uid) all check out.

Two non-blocking points, both left inline on domain/solution.rs:

  • Wire format uses raw u64 numbers for prices / executedAmount / fee, while the EVM solvers-dto serializes these as HexOrDecimalU256 strings. Worth aligning — both for consistency with the existing /solve contract and to avoid >2^53 precision loss for any non-Rust JSON consumer.
  • Minor: Interaction::custom clones each instruction's data/accounts even though swap is owned and dropped right after; into_iter() + move would avoid the copies.

Nothing here blocks merge.
· solana-solvers/pr3-solution-assembly

Comment on lines +22 to +23
#[serde_as(as = "HashMap<serde_with::DisplayFromStr, _>")]
pub prices: HashMap<Pubkey, u64>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prices (and Trade::executed_amount / fee below) go on the wire as raw u64 JSON numbers, whereas the established /solve DTO in crates/solvers-dto/src/solution.rs serializes every price/amount/fee as a HexOrDecimalU256 string:

#[serde_as(as = "HashMap<_, HexOrDecimalU256>")]
pub prices: HashMap<Address, U256>,
...
#[serde_as(as = "HexOrDecimalU256")]
pub executed_amount: U256,

Two concerns:

  1. Consistency — the driver already understands the EVM wire convention (decimal/hex strings). Emitting bare numbers means the Solana /solve contract diverges from the rest of the protocol for no stated reason (the description pins the wire shape but doesn't call out the number-vs-string choice).
  2. Precision — a Solana token amount / price is a full u64, whose max (~1.8e19) exceeds 2^53. Any non-Rust JSON consumer that reads these as IEEE-754 doubles will silently corrupt large values. Strings sidestep that.

Suggest serializing these as strings to match the EVM DTO. Fix this →

executed_amount,
fee: 0,
}],
interactions: swap.instructions.iter().map(Interaction::custom).collect(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: swap is owned here and swap.instructions is never read again, but Interaction::custom takes &Instruction and so clones data and rebuilds every AccountMeta. Consuming the vec by value would let you move the data out instead of cloning:

interactions: swap.instructions.into_iter().map(Interaction::custom).collect(),

with custom(instruction: Instruction) moving instruction.data. This requires reading swap.instructions before swap.address_lookup_tables is moved (already the case in this struct literal).

Comment on lines +22 to +23
#[serde_as(as = "HashMap<serde_with::DisplayFromStr, _>")]
pub prices: HashMap<Pubkey, u64>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prices (and Trade::executed_amount / fee below) go on the wire as raw u64 JSON numbers, whereas the established /solve DTO in crates/solvers-dto/src/solution.rs serializes every price/amount/fee as a HexOrDecimalU256 string:

#[serde_as(as = "HashMap<_, HexOrDecimalU256>")]
pub prices: HashMap<Address, U256>,
...
#[serde_as(as = "HexOrDecimalU256")]
pub executed_amount: U256,

Two concerns:

  1. Consistency — the driver already understands the EVM wire convention (decimal/hex strings). Emitting bare numbers here means the Solana /solve contract diverges from the rest of the protocol for no stated reason (the PR description pins the wire shape but doesn't call out the number-vs-string choice).
  2. Precision — a Solana token amount / price is a full u64, whose max (~1.8e19) exceeds 2^53. Any non-Rust JSON consumer (or a generic JSON tool) that reads these as IEEE-754 doubles will silently corrupt large values. Strings sidestep that entirely.

Suggest serializing these as strings to match the EVM DTO. Fix this →

executed_amount,
fee: 0,
}],
interactions: swap.instructions.iter().map(Interaction::custom).collect(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: swap is owned here and swap.instructions is never read again, but Interaction::custom takes &Instruction and so clones data and rebuilds every AccountMeta. Consuming the vec by value would let you move the data out instead of cloning:

interactions: swap.instructions.into_iter().map(Interaction::custom).collect(),

with custom(instruction: Instruction) moving instruction.data. Note this requires reading swap.instructions before swap.address_lookup_tables is moved (already the case in this struct literal).

Comment thread crates/solana-solvers/src/domain/order.rs Outdated
Comment thread crates/solana-solvers/src/domain/solution.rs Outdated
Comment on lines +76 to +79
/// Sell and buy mint coincide, so a uniform clearing-price map (one price
/// per mint) cannot represent the trade.
#[error("sell and buy mint are the same")]
SameMint,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given all the effort done into removing UCPs, did we check with solvers if we should actually be adding the UCP concept back in?

@squadgazzz squadgazzz Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Dropped UCPs from the DTO. It looks like they won't be used at all.

Comment thread crates/solana-solvers/src/domain/solution.rs Outdated
Comment thread crates/solana-solvers/src/dto/solution.rs

@jmg-duarte jmg-duarte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@MartinquaXD MartinquaXD left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving to not be a blocker on the solana development.

Comment thread crates/solana-solvers/src/dto/order.rs
Comment on lines +22 to +24
/// Optional solver estimate of total settlement compute units.
#[serde(skip_serializing_if = "Option::is_none")]
pub cu_estimate: Option<u64>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't use the cu_estimate anyway we might as well not parse it, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need it for quoting, IIUC.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. AFIAU solana does not have all the tools we'd need to do our quote verification and determine the true CUs ourselves. 👌

Comment thread crates/solana-solvers/src/domain/mod.rs Outdated
Comment thread crates/solana-solvers/src/domain/solution.rs Outdated
Comment thread crates/solana-solvers/src/dto/solution.rs Outdated
@linear-code

linear-code Bot commented Jul 24, 2026

Copy link
Copy Markdown

BE-144

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants