Repository Public
1// Solana same-chain swaps through the Jupiter aggregator program (Swap API V2 "Router", GET /build — labelled "Metis").2//3// Jupiter's answer is NOT trusted. The API assembles a Solana **v1 transaction** (SIMD-0385: every account address is4// inline, there are NO address lookup tables, the compute budget sits in the message header) and the signer decodes the5// exact message bytes itself. Refusing v0 removes the lookup-table problem altogether: a table's content cannot be6// verified offline, and with v1 there is nothing behind a table — every account the transaction can touch is visible.7//8// What the decoder proves, from the bytes alone:9// - one required signature, the fee payer, and it is the wallet; a blockhash; a bounded priority fee;10// - the instruction list is exactly: [ATA CreateIdempotent for the wallet's own account of the input/output mint]*,11// [System.Transfer wallet -> its own wSOL account for exactly the input amount, Token.SyncNative] when SOL is sold,12// ONE Jupiter instruction, `shared_accounts_route_v2` only, [Token.CloseAccount of the wallet's wSOL account13// back to the wallet] when SOL is sold or bought. Nothing else: no other program, no transfer, approve or authority change;14// - in the Jupiter instruction: authority = wallet, source = the wallet's associated token account of the input mint,15// destination = the wallet's associated token account of the output mint, both mints and their token programs, no16// alternate destination, `in_amount` = the approved amount, floor(quoted_out × (10 000 − slippage_bps) / 10 000) ≥ the17// approved minimum, `platform_fee_bps` = 0, `positive_slippage_bps` = 0;18// - among the route's remaining accounts: neither the wallet nor its source/destination account, and nowhere in the19// transaction any other registry token account of the wallet. The wallet's signature therefore authorises one thing20// only: the program's own pull of `in_amount` from the source account. `route_v2` (venue programs act with the21// wallet's signature) is refused; the API rewrites such routes into the shared-accounts form before asking.22//23// What it cannot prove (residual trust, see docs/swap/providers/jupiter.md): the aggregator program's own behaviour. The24// program's source was not read and it is upgradeable; its layout is taken from its on-chain IDL and its behaviour was checked by25// simulation. It is the program that takes at most `in_amount` and reverts below the minimum. The API pins the program's26// last deployment (JUPITER_PIN) and refuses to prepare a swap if it changed.27import { sha256 } from '@noble/hashes/sha2.js';28import { base58 } from '@scure/base';29import { baseUnits } from './amounts.ts';30import { ATA_PROGRAM, associatedTokenAddress, findProgramAddress, SYSTEM_PROGRAM, TOKEN_2022_PROGRAM, TOKEN_PROGRAM, tokenProgramOf } from './build-sol.ts';31import { getAsset, getNetwork } from './networks.ts';32import type { Intent } from './intent.ts';3334export const JUPITER_PROGRAM = 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4';35/** PDA ["__event_authority"] of the program (constant in the on-chain IDL). */36export const JUPITER_EVENT_AUTHORITY = 'D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf';37export const WSOL_MINT = 'So11111111111111111111111111111111111111112';38export const BPF_UPGRADEABLE_LOADER = 'BPFLoaderUpgradeab1e11111111111111111111111';39/**40 * The reviewed deployment of the aggregator program, read on two RPC endpoints on 2026-11-10. The program is upgradeable:41 * whoever holds `upgradeAuthority` can replace it. An upgrade rewrites the ProgramData header (`lastDeploySlot`), so the42 * API refuses to prepare a swap unless the header still shows this slot and authority on every endpoint, and periodically43 * re-hashes the whole executable.44 */45export const JUPITER_PIN = {46 programData: '4Ec7ZxZS6Sbdg5UGSLHbAnM7GQHp2eFd4KYWRexAipQT',47 lastDeploySlot: 462802085,48 upgradeAuthority: 'CvQZZ23qYDWF2RUpxYJ8y9K4skmuvYEEjH7fK58jtipQ',49 /** SHA-256 of the executable bytes (ProgramData account data after its 45-byte header), 2 892 224 bytes */50 codeSha256: '53d53f447f594158a259cbdca1f5e613caf40bc14ce759ffd5953580dc2af8dd',51 reviewed: '2026-11-10: on-chain IDL read (route_v2, shared_accounts_route_v2), behaviour checked by mainnet simulation (scripts/sim-jupiter.mts)',52} as const;5354/** Anchor discriminators = sha256("global:<name>")[0..8]; both instructions are in the program's on-chain IDL. */55const DISC_ROUTE_V2 = 'bb64facc31c4af14', DISC_SHARED_ROUTE_V2 = 'd19853937cfed8e9';56export const RENT_TOKEN_ACCOUNT = 2_039_280n; // bound: rent-exempt minimum of a 165-byte SPL Token account at the historical rate (1 488 440 measured on 2026-11-10)57/** Token-2022 accounts carry extensions; this bounds an account of up to 231 bytes at the historical rent rate. */58export const RENT_TOKEN2022_ACCOUNT_MAX = 2_498_640n;59export const SOL_SIGNATURE_FEE = 5000n;60const MAX_COMPUTE_UNITS = 1_400_000, MAX_LOADED_ACCOUNTS_DATA = 64 * 1024 * 1024;61/** v1 config mask: priority fee (bits 0-1), compute-unit limit (bit 2), loaded-accounts data size limit (bit 3). No heap request. */62const V1_CONFIG_MASK = 0x0f;63const V1_VERSION_BYTE = 0x81;6465export interface SolIxInput { program: string; accounts: { pubkey: string; signer: boolean; writable: boolean }[]; data: Uint8Array }66export interface SolV1Ix { program: string; accounts: string[]; data: Uint8Array }67export interface SolV1Message {68 payer: string; blockhash: string; priorityFeeLamports: bigint; computeUnitLimit: number; loadedAccountsDataSizeLimit: number;69 keys: string[]; numSigners: number; numReadonlySigned: number; numReadonlyUnsigned: number; instructions: SolV1Ix[];70}7172const cat = (parts: Uint8Array[]) => { const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); let o = 0; for (const p of parts) { out.set(p, o); o += p.length; } return out; };73const hex = (b: Uint8Array) => Buffer.from(b).toString('hex');74const pk = (s: string) => { const b = base58.decode(s); if (b.length !== 32) throw new Error('bad public key'); return b; };7576/** base58 form of a 32-byte key or blockhash */77export const solAddress = (b: Uint8Array): string => { if (b.length !== 32) throw new Error('bad public key'); return base58.encode(b); };78/** sha256 of the message bytes, lowercase hex: this is what the approved intent commits to. */79export const solMessageHash = (msg: Uint8Array): string => hex(sha256(msg));80/** v1 wire format: the message, then the signatures (no count prefix: the count is the first header byte). */81export function solV1Wire(signature: Uint8Array, message: Uint8Array): Uint8Array {82 if (signature.length !== 64 || message[0] !== V1_VERSION_BYTE || message[1] !== 1) throw new Error('bad v1 transaction');83 return cat([message, signature]);84}8586/**87 * Assemble a v1 message with a single signer (the fee payer). Key order: payer, writable accounts, read-only accounts, each88 * in order of first use. Throws if any instruction asks for another signer.89 */90export function compileSolV1Message(o: { payer: string; blockhash: string; instructions: SolIxInput[]; computeUnitLimit: number; priorityFeeLamports: bigint; loadedAccountsDataSizeLimit: number }): Uint8Array {91 const writable = new Set<string>(), order: string[] = [];92 const see = (k: string) => { if (k !== o.payer && !order.includes(k)) order.push(k); };93 for (const ix of o.instructions) {94 for (const a of ix.accounts) { if (a.signer && a.pubkey !== o.payer) throw new Error('instruction requires another signer'); see(a.pubkey); if (a.writable) writable.add(a.pubkey); }95 see(ix.program);96 }97 const w = order.filter((k) => writable.has(k)), r = order.filter((k) => !writable.has(k));98 const keys = [o.payer, ...w, ...r];99 if (keys.length > 64) throw new Error('too many accounts for a v1 transaction (64)');100 if (o.instructions.length > 64 || o.instructions.length === 0) throw new Error('bad instruction count');101 const idx = (k: string) => { const n = keys.indexOf(k); if (n < 0) throw new Error('unknown key'); return n; };102 const head = new Uint8Array(1 + 3 + 4 + 32 + 2), dv = new DataView(head.buffer);103 head[0] = V1_VERSION_BYTE; head[1] = 1; head[2] = 0; head[3] = r.length;104 dv.setUint32(4, V1_CONFIG_MASK, true);105 const bh = pk(o.blockhash); head.set(bh, 8);106 head[40] = o.instructions.length; head[41] = keys.length;107 const cfg = new Uint8Array(16), cv = new DataView(cfg.buffer);108 if (o.priorityFeeLamports < 0n || o.priorityFeeLamports >= 1n << 64n) throw new Error('bad priority fee');109 cv.setBigUint64(0, o.priorityFeeLamports, true); cv.setUint32(8, o.computeUnitLimit, true); cv.setUint32(12, o.loadedAccountsDataSizeLimit, true);110 const headers: Uint8Array[] = [], payloads: Uint8Array[] = [];111 for (const ix of o.instructions) {112 if (ix.accounts.length > 255 || ix.data.length > 0xffff) throw new Error('instruction too large');113 const h = new Uint8Array(4); h[0] = idx(ix.program); h[1] = ix.accounts.length; new DataView(h.buffer).setUint16(2, ix.data.length, true);114 headers.push(h); payloads.push(Uint8Array.from(ix.accounts.map((a) => idx(a.pubkey))), ix.data);115 }116 const out = cat([head, ...keys.map(pk), cfg, ...headers, ...payloads]);117 if (out.length + 64 > 4096) throw new Error('transaction larger than 4096 bytes');118 return out;119}120121/** Strict decoder of a v1 message: exact length, known config mask, unique keys, indices in range. Throws otherwise. */122export function decodeSolV1Message(msg: Uint8Array): SolV1Message {123 if (msg.length < 42 + 32 + 16 || msg.length + 64 > 4096) throw new Error('message size out of range');124 if (msg[0] !== V1_VERSION_BYTE) throw new Error('not a v1 transaction message (lookup-table transactions are refused)');125 const dv = new DataView(msg.buffer, msg.byteOffset, msg.byteLength);126 const numSigners = msg[1]!, numReadonlySigned = msg[2]!, numReadonlyUnsigned = msg[3]!;127 if (dv.getUint32(4, true) !== V1_CONFIG_MASK) throw new Error('unexpected transaction config mask');128 const blockhash = base58.encode(msg.subarray(8, 40));129 const nIx = msg[40]!, nKeys = msg[41]!;130 if (nIx < 1 || nIx > 64 || nKeys < 2 || nKeys > 64) throw new Error('instruction or account count out of range');131 if (numSigners < 1 || numSigners > nKeys || numReadonlySigned >= numSigners || numReadonlyUnsigned > nKeys - numSigners) throw new Error('bad message header');132 let o = 42;133 const need = (n: number) => { if (o + n > msg.length) throw new Error('truncated message'); };134 need(nKeys * 32 + 16 + nIx * 4);135 const keys: string[] = [];136 for (let k = 0; k < nKeys; k++, o += 32) keys.push(base58.encode(msg.subarray(o, o + 32)));137 if (new Set(keys).size !== keys.length) throw new Error('duplicate account keys');138 const priorityFeeLamports = dv.getBigUint64(o, true), computeUnitLimit = dv.getUint32(o + 8, true), loadedAccountsDataSizeLimit = dv.getUint32(o + 12, true); o += 16;139 const heads: { p: number; n: number; d: number }[] = [];140 for (let k = 0; k < nIx; k++, o += 4) heads.push({ p: msg[o]!, n: msg[o + 1]!, d: dv.getUint16(o + 2, true) });141 const instructions: SolV1Ix[] = [];142 for (const h of heads) {143 need(h.n + h.d);144 if (h.p >= nKeys || h.p === 0) throw new Error('bad program index');145 const accounts: string[] = [];146 for (let k = 0; k < h.n; k++) { const a = msg[o + k]!; if (a >= nKeys) throw new Error('bad account index'); accounts.push(keys[a]!); }147 o += h.n;148 instructions.push({ program: keys[h.p]!, accounts, data: msg.slice(o, o + h.d) }); o += h.d;149 }150 if (o !== msg.length) throw new Error('trailing bytes after the message');151 return { payer: keys[0]!, blockhash, priorityFeeLamports, computeUnitLimit, loadedAccountsDataSizeLimit, keys, numSigners, numReadonlySigned, numReadonlyUnsigned, instructions };152}153154/** The minimum output the signer relies on: never above what the program enforces (floor of the exact product). */155export const jupiterMinOut = (quotedOut: bigint, slippageBps: number): bigint => (quotedOut * BigInt(10_000 - slippageBps)) / 10_000n;156/** Program authority PDAs: seeds ["authority", id]. */157export const jupiterProgramAuthority = (id: number): string => findProgramAddress([new TextEncoder().encode('authority'), Uint8Array.of(id)], JUPITER_PROGRAM);158159export interface JupiterRoute { kind: 'route_v2' | 'shared_accounts_route_v2'; inAmount: bigint; quotedOut: bigint; slippageBps: number; minOut: bigint; legs: number; accounts: number }160/** Decode the fixed head of a whitelisted Jupiter instruction. The route plan after it stays opaque. */161export function decodeJupiterRoute(data: Uint8Array, accounts: number): JupiterRoute & { authorityId: number | null } {162 const disc = hex(data.subarray(0, 8));163 const shared = disc === DISC_SHARED_ROUTE_V2;164 if (!shared && disc !== DISC_ROUTE_V2) throw new Error('Jupiter instruction type is not allowed');165 const o = shared ? 9 : 8;166 if (data.length < o + 26 + 5) throw new Error('Jupiter instruction data too short');167 const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);168 const inAmount = dv.getBigUint64(o, true), quotedOut = dv.getBigUint64(o + 8, true);169 const slippageBps = dv.getUint16(o + 16, true), platformFeeBps = dv.getUint16(o + 18, true), positiveSlippageBps = dv.getUint16(o + 20, true), legs = dv.getUint32(o + 22, true);170 if (platformFeeBps !== 0) throw new Error('Jupiter instruction carries a platform fee');171 if (positiveSlippageBps !== 0) throw new Error('Jupiter instruction carries a positive-slippage fee');172 if (slippageBps > 10_000) throw new Error('slippage out of range');173 if (legs < 1 || legs > 32) throw new Error('route plan length out of range');174 return { kind: shared ? 'shared_accounts_route_v2' : 'route_v2', authorityId: shared ? data[8]! : null, inAmount, quotedOut, slippageBps, minOut: jupiterMinOut(quotedOut, slippageBps), legs, accounts };175}176177export interface JupiterSwapFacts extends JupiterRoute {178 message: SolV1Message; sourceAccount: string; destinationAccount: string; inMint: string; outMint: string;179 /** token accounts the transaction may create and leave open (rent stays in the wallet's own account) */180 creates: string[]; wrapsSol: boolean; unwrapsSol: boolean;181 /** signature fee + priority fee, exact */ networkFee: bigint; /** upper bound of the rent locked in accounts created and kept */ rentMax: bigint;182}183184const mintOf = (a: { id: string }) => (a.id === 'native' ? WSOL_MINT : a.id);185const programOf = (a: { id: string; program?: 'token' | 'token2022' }) => (a.id === 'native' ? TOKEN_PROGRAM : tokenProgramOf(a));186187/**188 * Decode the message and check every value-moving element against the approved intent. Throws on any mismatch.189 * `i.from` stands for the wallet: the signer has already checked that it is its own address.190 */191export function verifyJupiterSwap(i: Intent, msg: Uint8Array): JupiterSwapFacts {192 const n = getNetwork(i.network), s = i.swap;193 if (n.family !== 'sol' || !i.sol || !s) throw new Error('not a Solana swap intent');194 if (i.bridge || i.evm || i.btc || i.x || i.memo !== undefined) throw new Error('unexpected intent section');195 if (s.provider !== 'jupiter') throw new Error('unknown swap provider');196 if (i.chainRef !== n.chainRef) throw new Error('chain identity mismatch');197 if (s.router !== JUPITER_PROGRAM) throw new Error('program is not the pinned Jupiter aggregator');198 if (s.approveGas !== '0') throw new Error('a Solana swap has no approval step');199 if (i.to !== i.from) throw new Error('a swap must deliver to the wallet itself');200 const me = i.from; pk(me);201 const src = getAsset(i.network, i.asset), dst = getAsset(i.network, s.toAsset); // throws unless both are in the registry202 if (src.decimals !== i.decimals || src.symbol !== i.symbol) throw new Error('input asset metadata mismatch');203 if (src.id === dst.id) throw new Error('input and output are the same asset');204 const amount = baseUnits(i.amount), minOut = baseUnits(s.minOut);205 if (amount <= 0n || minOut <= 0n || amount >= 1n << 64n) throw new Error('amounts out of range');206 if (solMessageHash(msg) !== s.dataHash) throw new Error('message does not match the approved hash');207208 const m = decodeSolV1Message(msg);209 if (m.numSigners !== 1 || m.numReadonlySigned !== 0) throw new Error('the transaction must have exactly one signer');210 if (m.payer !== me) throw new Error('fee payer is not the wallet');211 if (m.blockhash !== i.sol.recentBlockhash || /^1{32,44}$/.test(m.blockhash)) throw new Error('blockhash does not match the approved one');212 if (i.sol.computeUnitLimit !== m.computeUnitLimit || m.computeUnitLimit < 1 || m.computeUnitLimit > MAX_COMPUTE_UNITS) throw new Error('compute-unit limit does not match the approved one');213 const priority = (BigInt(m.computeUnitLimit) * baseUnits(i.sol.computeUnitPrice) + 999_999n) / 1_000_000n;214 if (m.priorityFeeLamports !== priority) throw new Error('priority fee does not match the approved compute-unit price');215 if (m.loadedAccountsDataSizeLimit < 1 || m.loadedAccountsDataSizeLimit > MAX_LOADED_ACCOUNTS_DATA) throw new Error('loaded-accounts data limit out of range');216217 const inMint = mintOf(src), outMint = mintOf(dst), inTp = programOf(src), outTp = programOf(dst);218 const srcAta = associatedTokenAddress(me, inMint, inTp), dstAta = associatedTokenAddress(me, outMint, outTp);219 const wsolAta = src.id === 'native' ? srcAta : dst.id === 'native' ? dstAta : null;220 const eq = (a: string[], b: string[]) => a.length === b.length && a.every((x, k) => x === b[k]);221 const writable = (k: string) => { const p = m.keys.indexOf(k); return p >= 0 && p < m.keys.length - m.numReadonlyUnsigned; };222223 // --- instruction grammar: creates*, [wrap], jupiter, [unwrap]224 const ixs = m.instructions;225 let p = 0;226 const creates: string[] = [];227 for (; p < ixs.length && ixs[p]!.program === ATA_PROGRAM; p++) {228 const ix = ixs[p]!;229 if (ix.data.length !== 1 || ix.data[0] !== 1) throw new Error('token-account instruction is not CreateIdempotent');230 const which = eq(ix.accounts, [me, srcAta, me, inMint, SYSTEM_PROGRAM, inTp]) ? srcAta : eq(ix.accounts, [me, dstAta, me, outMint, SYSTEM_PROGRAM, outTp]) ? dstAta : null;231 if (!which) throw new Error('token-account creation is not for the wallet\'s own account of the swapped assets');232 if (creates.includes(which)) throw new Error('duplicate token-account creation');233 creates.push(which);234 }235 let wrapsSol = false;236 if (src.id === 'native') {237 const t = ixs[p], y = ixs[p + 1];238 if (!t || !y || t.program !== SYSTEM_PROGRAM || y.program !== TOKEN_PROGRAM) throw new Error('missing the SOL wrap instructions');239 const td = new DataView(t.data.buffer, t.data.byteOffset, t.data.byteLength);240 if (t.data.length !== 12 || td.getUint32(0, true) !== 2 || !eq(t.accounts, [me, srcAta])) throw new Error('SOL transfer is not into the wallet\'s own wrapped-SOL account');241 if (td.getBigUint64(4, true) !== amount) throw new Error('wrapped amount differs from the approved amount');242 if (y.data.length !== 1 || y.data[0] !== 17 || !eq(y.accounts, [srcAta])) throw new Error('unexpected instruction after the SOL transfer');243 if (!creates.includes(srcAta)) throw new Error('the wrapped-SOL account must be created (idempotently) in the same transaction');244 wrapsSol = true; p += 2;245 }246 const jup = ixs[p++];247 if (!jup || jup.program !== JUPITER_PROGRAM) throw new Error(jup ? `instruction for an unexpected program ${jup.program}` : 'no Jupiter instruction');248 let unwrapsSol = false;249 if (wsolAta) {250 const c = ixs[p++];251 if (!c || c.program !== TOKEN_PROGRAM || c.data.length !== 1 || c.data[0] !== 9 || !eq(c.accounts, [wsolAta, me, me])) throw new Error('missing the wrapped-SOL close back to the wallet');252 unwrapsSol = true;253 }254 if (p !== ixs.length) throw new Error(ixs[p]!.program === JUPITER_PROGRAM ? 'more than one Jupiter instruction' : `instruction for an unexpected program ${ixs[p]!.program}`);255 if (dst.id === 'native' && !creates.includes(dstAta)) throw new Error('the wrapped-SOL account must be created (idempotently) in the same transaction');256257 // --- the Jupiter instruction258 const r = decodeJupiterRoute(jup.data, jup.accounts.length);259 // `route_v2` hands the wallet's signature to every venue program of the route, and the program does not tie those legs to260 // the declared source account (shown by simulation: a route declared USDC -> SOL spent USDT). It is never signed.261 if (r.kind !== 'shared_accounts_route_v2') throw new Error('this Jupiter instruction type lets the route act with the wallet\'s signature; only the shared-accounts form is signed');262 const a = jup.accounts, named = 12;263 const authority = jupiterProgramAuthority(r.authorityId!);264 const want = [authority, me, srcAta, associatedTokenAddress(authority, inMint, inTp), associatedTokenAddress(authority, outMint, outTp), dstAta, inMint, outMint, inTp, outTp, JUPITER_EVENT_AUTHORITY, JUPITER_PROGRAM];265 if (a.length < named || !eq(a.slice(0, named), want)) throw new Error(describeMismatch(a, me, srcAta, dstAta));266 if (r.inAmount !== amount) throw new Error('the instruction spends another amount');267 if (r.minOut < minOut) throw new Error('the instruction guarantees less than the approved minimum');268 // The wallet signs for one thing only: the program's own pull of `in_amount` from the source account. Neither the wallet269 // nor its two token accounts may appear among the route's accounts, so no venue program ever runs with its signature.270 if (a.slice(named).some((k) => k === me || k === srcAta || k === dstAta)) throw new Error('the route reaches into the wallet\'s accounts');271 // and no other registry token account of the wallet may be referenced anywhere in the transaction272 const others = new Set<string>();273 for (const t of [{ id: WSOL_MINT }, ...n.tokens]) for (const tp of [TOKEN_PROGRAM, TOKEN_2022_PROGRAM]) { const x = associatedTokenAddress(me, t.id, tp); if (x !== srcAta && x !== dstAta) others.add(x); }274 for (const k of m.keys) if (others.has(k)) throw new Error('the transaction references another token account of the wallet');275 if (!writable(srcAta) || !writable(dstAta)) throw new Error('source or destination account is not writable');276277 // --- cost: exact network fee, plus the rent of accounts created and kept (the wrapped-SOL account is closed again)278 const networkFee = SOL_SIGNATURE_FEE + m.priorityFeeLamports;279 const kept = creates.filter((c) => c !== wsolAta);280 const rentMax = kept.reduce((t, c) => t + ((c === srcAta ? inTp : outTp) === TOKEN_2022_PROGRAM ? RENT_TOKEN2022_ACCOUNT_MAX : RENT_TOKEN_ACCOUNT), 0n);281 if (i.sol.createAta !== kept.length > 0) throw new Error('token-account creation does not match the approved intent');282 if (networkFee + rentMax > baseUnits(i.maxFee)) throw new Error('fee exceeds approved maximum');283 return { ...r, message: m, sourceAccount: srcAta, destinationAccount: dstAta, inMint, outMint, creates: kept, wrapsSol, unwrapsSol, networkFee, rentMax };284}285286function describeMismatch(a: string[], me: string, srcAta: string, dstAta: string): string {287 if (a[1] !== me) return 'the transfer authority is not the wallet';288 if (a[2] !== srcAta) return 'the source token account is not the wallet\'s own account of the input asset';289 if (a[5] !== dstAta) return 'the destination token account is not the wallet\'s own account of the output asset';290 return 'the instruction\'s fixed accounts are not the expected ones';291}292293/**294 * API side only (never used by the signer): rewrite a `route_v2` instruction into `shared_accounts_route_v2`, the form the295 * signer accepts. Same route plan; the wallet and its token accounts inside the legs are replaced by the program authority296 * `id` and its own token accounts, so the venues run with the program's signature instead of the wallet's. `extra` maps297 * further wallet token accounts (intermediate mints) to the program's. Whether the result works is decided by simulation.298 */299export function jupiterRouteToShared(ix: SolIxInput, id: number, extra: Record<string, string> = {}): SolIxInput {300 if (ix.program !== JUPITER_PROGRAM || hex(ix.data.subarray(0, 8)) !== DISC_ROUTE_V2 || ix.accounts.length < 10) throw new Error('not a route_v2 instruction');301 const [me, src, dst, inMint, outMint, inTp, outTp, alt] = ix.accounts.slice(0, 8).map((x) => x.pubkey) as [string, string, string, string, string, string, string, string];302 if (alt !== JUPITER_PROGRAM) throw new Error('route_v2 instruction names an alternate destination');303 const auth = jupiterProgramAuthority(id), pSrc = associatedTokenAddress(auth, inMint, inTp), pDst = associatedTokenAddress(auth, outMint, outTp);304 const ro = (pubkey: string) => ({ pubkey, signer: false, writable: false }), rw = (pubkey: string) => ({ pubkey, signer: false, writable: true });305 const swap: Record<string, string> = { ...extra, [me]: auth, [src]: pSrc, [dst]: pDst };306 const data = new Uint8Array(ix.data.length + 1);307 data.set(Buffer.from(DISC_SHARED_ROUTE_V2, 'hex')); data[8] = id; data.set(ix.data.subarray(8), 9);308 return { program: JUPITER_PROGRAM, data, accounts: [ro(auth), { pubkey: me, signer: true, writable: false }, rw(src), rw(pSrc), rw(pDst), rw(dst), ro(inMint), ro(outMint), ro(inTp), ro(outTp), ro(JUPITER_EVENT_AUTHORITY), ro(JUPITER_PROGRAM),309 ...ix.accounts.slice(10).map((x) => (swap[x.pubkey] ? { pubkey: swap[x.pubkey]!, signer: false, writable: x.writable } : x))] };310}311312/**313 * API side only: put Jupiter's instructions in the order the signer's grammar expects (token-account creations, wrap,314 * swap, unwrap) and, when the swap instruction is `route_v2`, rewrite it to the shared-accounts form with program315 * authority `authorityId`. Creations of the wallet's accounts for intermediate mints are dropped: the rewritten route uses316 * the program's own accounts for them. Nothing here is trusted by the signer.317 */318export function arrangeJupiterInstructions(o: { setup: SolIxInput[]; swap: SolIxInput; cleanup: SolIxInput | null; authorityId: number }): { instructions: SolIxInput[]; converted: boolean; kind: JupiterRoute['kind']; programAccounts: string[] } {319 const kind = decodeJupiterRoute(o.swap.data, o.swap.accounts.length).kind;320 const pos = kind === 'route_v2' ? { me: 0, src: 1, dst: 2 } : { me: 1, src: 2, dst: 5 };321 const me = o.swap.accounts[pos.me]!.pubkey, src = o.swap.accounts[pos.src]!.pubkey, dst = o.swap.accounts[pos.dst]!.pubkey;322 const creates = o.setup.filter((x) => x.program === ATA_PROGRAM), other = o.setup.filter((x) => x.program !== ATA_PROGRAM);323 const own = creates.filter((x) => x.accounts[1]?.pubkey === src || x.accounts[1]?.pubkey === dst);324 let swap = o.swap, converted = false;325 if (kind === 'route_v2') {326 const auth = jupiterProgramAuthority(o.authorityId), extra: Record<string, string> = {};327 for (const c of creates) if (!own.includes(c) && c.accounts.length === 6 && c.accounts[2]!.pubkey === me) extra[c.accounts[1]!.pubkey] = associatedTokenAddress(auth, c.accounts[3]!.pubkey, c.accounts[5]!.pubkey);328 swap = jupiterRouteToShared(o.swap, o.authorityId, extra); converted = true;329 }330 // the program's own token accounts this instruction relies on (they must exist): source, destination, intermediates331 const programAccounts = [swap.accounts[3]!.pubkey, swap.accounts[4]!.pubkey, ...(converted ? swap.accounts.slice(12).map((x) => x.pubkey).filter((k, n) => o.swap.accounts[10 + n]!.pubkey !== k && k !== swap.accounts[0]!.pubkey) : [])];332 return { instructions: [...own, ...other, swap, ...(o.cleanup ? [o.cleanup] : [])], converted, kind, programAccounts: [...new Set(programAccounts)] };333}334335/** The bytes the signer signs for a Jupiter swap: the verified message itself. */336export function buildSolSwapMessage(i: Intent, messageB64: string): Uint8Array {337 if (!/^[A-Za-z0-9+/]+={0,2}$/.test(messageB64) || messageB64.length > 5500) throw new Error('swap message is not base64');338 const msg = new Uint8Array(Buffer.from(messageB64, 'base64'));339 verifyJupiterSwap(i, msg);340 return msg;341}342