import {
AccountRole,
Address,
address,
AddressesByLookupTableAddress,
appendTransactionMessageInstructions,
Base64EncodedBytes,
Blockhash,
compileTransaction,
compressTransactionMessageUsingAddressLookupTables,
createKeyPairSignerFromBytes,
createSolanaRpc,
createTransactionMessage,
getBase58Decoder,
getBase58Encoder,
getBase64Codec,
getBase64EncodedWireTransaction,
AccountMeta,
Instruction,
pipe,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
signTransaction,
} from "@solana/kit";
// ── Types matching the /build response ───────────────────────────────────────
type Account = { pubkey: Address; isSigner: boolean; isWritable: boolean };
type ApiInstruction = {
programId: Address;
accounts: Account[];
data: Base64EncodedBytes;
};
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 COMPUTE_BUDGET_PROGRAM = address(
"ComputeBudget111111111111111111111111111111",
);
const CU_LIMIT_MAX = 1_400_000;
function createInstruction(ix: ApiInstruction): Instruction {
return {
programAddress: ix.programId,
accounts: ix.accounts.map((acc) => ({
address: acc.pubkey,
role: acc.isSigner && acc.isWritable
? AccountRole.WRITABLE_SIGNER
: acc.isSigner
? AccountRole.READONLY_SIGNER
: acc.isWritable
? AccountRole.WRITABLE
: AccountRole.READONLY,
})),
data: Uint8Array.from(getBase64Codec().encode(ix.data)),
};
}
function makeSetComputeUnitLimitIx(units: number): ApiInstruction {
const data = Buffer.alloc(5);
data.writeUInt8(0x02, 0);
data.writeUInt32LE(units, 1);
return {
programId: COMPUTE_BUDGET_PROGRAM,
accounts: [],
data: data.toString("base64") as ApiInstruction["data"],
};
}
function transformBlockhash(meta: BuildResponse["blockhashWithMetadata"]) {
return {
blockhash: getBase58Decoder().decode(
Uint8Array.from(meta.blockhash),
) as Blockhash,
lastValidBlockHeight: BigInt(meta.lastValidBlockHeight),
};
}
function transformALTs(
raw: Record<string, string[]> | null,
): AddressesByLookupTableAddress {
if (!raw) return {};
return Object.fromEntries(
Object.entries(raw).map(([key, addrs]) => [
address(key),
addrs.map((a) => address(a)),
]),
);
}
function buildTransaction(
ixs: Instruction[],
blockhash: { blockhash: Blockhash; lastValidBlockHeight: bigint },
alts: AddressesByLookupTableAddress,
feePayer: Address,
) {
return pipe(
createTransactionMessage({ version: 0 }),
(msg) => appendTransactionMessageInstructions(ixs, msg),
(msg) => compressTransactionMessageUsingAddressLookupTables(msg, alts),
(msg) => setTransactionMessageFeePayer(feePayer, msg),
(msg) => setTransactionMessageLifetimeUsingBlockhash(blockhash, msg),
(msg) => compileTransaction(msg),
);
}
// ── Main ─────────────────────────────────────────────────────────────────────
const API_KEY = process.env.JUPITER_API_KEY!;
const rpc = createSolanaRpc(process.env.RPC_URL!);
const signer = await createKeyPairSignerFromBytes(
getBase58Encoder().encode(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.address,
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(createInstruction),
createInstruction(build.swapInstruction),
...(build.cleanupInstruction
? [createInstruction(build.cleanupInstruction)]
: []),
...build.otherInstructions.map(createInstruction),
createInstruction(build.tipInstruction),
];
// 3. Prepare blockhash and address lookup tables
const blockhash = transformBlockhash(build.blockhashWithMetadata);
const alts = transformALTs(build.addressesByLookupTableAddress);
// 4. Simulate to estimate CU usage, then apply 1.2x buffer
const simTx = buildTransaction(
[createInstruction(makeSetComputeUnitLimitIx(CU_LIMIT_MAX)), ...instructions],
blockhash,
alts,
signer.address,
);
const sim = await rpc
.simulateTransaction(getBase64EncodedWireTransaction(simTx), {
encoding: "base64",
commitment: "confirmed",
replaceRecentBlockhash: true,
})
.send();
if (sim.value.err) {
console.error("Simulation failed:", sim.value.err);
process.exit(1);
}
const estimatedCUL = sim.value.unitsConsumed
? Math.min(
Math.ceil(Number(sim.value.unitsConsumed) * 1.2),
CU_LIMIT_MAX,
)
: CU_LIMIT_MAX;
// 5. Build final transaction with estimated CU limit + CU price from response
const compiledTx = buildTransaction(
[
createInstruction(makeSetComputeUnitLimitIx(estimatedCUL)),
...build.computeBudgetInstructions.map(createInstruction),
...instructions,
],
blockhash,
alts,
signer.address,
);
// 6. Sign and submit via /submit (or use your own RPC / transaction pipeline)
const signedTx = await signTransaction([signer.keyPair], compiledTx);
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: getBase64EncodedWireTransaction(signedTx),
}),
});
const { signature } = await submitRes.json();
console.log("Submitted:", `https://solscan.io/tx/${signature}`);
// 7. Confirm the transaction landed
const confirmation = await rpc
.confirmTransaction(signature, {
strategy: { type: "blockhash", ...blockhash },
commitment: "confirmed",
})
.send();
if (confirmation.value.err) {
console.error("Transaction failed:", confirmation.value.err);
process.exit(1);
}
console.log("Confirmed:", signature);