import {
AddressLookupTableAccount,
ComputeBudgetProgram,
Connection,
Keypair,
PublicKey,
TransactionInstruction,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
import bs58 from "bs58";
// ── Types matching the /build response ───────────────────────────────────────
type ApiAccount = { pubkey: string; isSigner: boolean; isWritable: boolean };
type ApiInstruction = {
programId: string;
accounts: ApiAccount[];
data: string; // base64
};
type BuildResponse = {
computeBudgetInstructions: ApiInstruction[];
setupInstructions: ApiInstruction[];
swapInstruction: ApiInstruction;
cleanupInstruction: ApiInstruction | null;
otherInstructions: ApiInstruction[];
tipInstruction: ApiInstruction;
addressesByLookupTableAddress: Record<string, string[]> | null;
blockhashWithMetadata: {
blockhash: number[];
lastValidBlockHeight: number;
};
};
// ── Helpers ──────────────────────────────────────────────────────────────────
const CU_LIMIT_MAX = 1_400_000;
function toInstruction(ix: ApiInstruction): TransactionInstruction {
return new TransactionInstruction({
programId: new PublicKey(ix.programId),
keys: ix.accounts.map((acc) => ({
pubkey: new PublicKey(acc.pubkey),
isSigner: acc.isSigner,
isWritable: acc.isWritable,
})),
data: Buffer.from(ix.data, "base64"),
});
}
// ── Main ─────────────────────────────────────────────────────────────────────
const API_KEY = process.env.JUPITER_API_KEY!;
const connection = new Connection(process.env.RPC_URL!);
const signer = Keypair.fromSecretKey(
bs58.decode(process.env.BS58_PRIVATE_KEY!),
);
// 1. Call /build with your swap parameters
const buildRes = await fetch(
"https://api.jup.ag/swap/v2/build?" +
new URLSearchParams({
inputMint: "So11111111111111111111111111111111111111112",
outputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
amount: "100000000",
taker: signer.publicKey.toString(),
tipAmount: "1000000",
}),
{ headers: { "x-api-key": API_KEY } },
);
const build: BuildResponse = await buildRes.json();
// 2. Collect instructions (excluding compute budget — we handle CU limit ourselves)
const instructions = [
...build.setupInstructions.map(toInstruction),
toInstruction(build.swapInstruction),
...(build.cleanupInstruction
? [toInstruction(build.cleanupInstruction)]
: []),
...build.otherInstructions.map(toInstruction),
toInstruction(build.tipInstruction),
];
// 3. Resolve address lookup tables
const addressLookupTableAccounts: AddressLookupTableAccount[] = [];
for (const addr of Object.keys(build.addressesByLookupTableAddress ?? {})) {
const result = await connection.getAddressLookupTable(new PublicKey(addr));
if (result.value) addressLookupTableAccounts.push(result.value);
}
// 4. Transform blockhash from byte array to base58
const recentBlockhash = bs58.encode(
Buffer.from(build.blockhashWithMetadata.blockhash),
);
const { lastValidBlockHeight } = build.blockhashWithMetadata;
// 5. Simulate with max CU limit to estimate actual usage
const simMessage = new TransactionMessage({
payerKey: signer.publicKey,
recentBlockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: CU_LIMIT_MAX }),
...instructions,
],
}).compileToV0Message(addressLookupTableAccounts);
const sim = await connection.simulateTransaction(
new VersionedTransaction(simMessage),
{ sigVerify: false, replaceRecentBlockhash: true },
);
if (sim.value.err) {
console.error("Simulation failed:", sim.value.err);
process.exit(1);
}
// 6. Set CU limit to 120% of simulated usage (capped at max)
const estimatedCUL = sim.value.unitsConsumed
? Math.min(Math.ceil(sim.value.unitsConsumed * 1.2), CU_LIMIT_MAX)
: CU_LIMIT_MAX;
// 7. Build final transaction: our CU limit + API's CU price + swap instructions
const messageV0 = new TransactionMessage({
payerKey: signer.publicKey,
recentBlockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: estimatedCUL }),
...build.computeBudgetInstructions.map(toInstruction),
...instructions,
],
}).compileToV0Message(addressLookupTableAccounts);
const transaction = new VersionedTransaction(messageV0);
transaction.sign([signer]);
// 8. Submit via /submit
const submitRes = await fetch("https://api.jup.ag/tx/v1/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": API_KEY,
},
body: JSON.stringify({
signedTransaction: Buffer.from(transaction.serialize()).toString("base64"),
}),
});
const { signature } = await submitRes.json();
console.log("Transaction:", `https://solscan.io/tx/${signature}`);