Skip to content

Commit

Permalink
add lil jit (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
amilz authored Oct 10, 2024
1 parent 84aa3c9 commit db64d28
Show file tree
Hide file tree
Showing 2 changed files with 276 additions and 2 deletions.
273 changes: 273 additions & 0 deletions solana/web3.js-2.0/jito-bundles/lilJit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import {
Rpc,
createDefaultRpcTransport,
createRpc,
createRpcApi,
Address,
mainnet,
Base58EncodedBytes,
createSolanaRpc,
createKeyPairSignerFromBytes,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
pipe,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
TransactionPartialSigner,
signTransactionMessageWithSigners,
getBase64EncodedWireTransaction,
Base64EncodedWireTransaction,
getTransactionEncoder,
getBase58Decoder,
} from "@solana/web3.js";
import secret from "./secret.json";
import { getAddMemoInstruction } from "@solana-program/memo";
import { getTransferSolInstruction } from "@solana-program/system";

const MINIMUM_JITO_TIP = 1_000; // lamports
const NUMBER_TRANSACTIONS = 5;
const SIMULATE_ONLY = true;
const ENDPOINT = 'https://example.quiknode.pro/123456/'; // 👈 Replace with your own endpoint
const POLL_INTERVAL_MS = 3000;
const POLL_TIMEOUT_MS = 30000;

type JitoBundleSimulationResponse = {
context: {
apiVersion: string;
slot: number;
};
value: {
summary: 'succeeded' | {
failed: {
error: {
TransactionFailure: [number[], string];
};
tx_signature: string;
};
};
transactionResults: Array<{
err: null | unknown;
logs: string[];
postExecutionAccounts: null | unknown;
preExecutionAccounts: null | unknown;
returnData: null | unknown;
unitsConsumed: number;
}>;
};
};

type LilJitAddon = {
getRegions(): string[];
getTipAccounts(): Address[];
getBundleStatuses(bundleIds: string[]): {
context: { slot: number };
value: {
bundleId: string;
transactions: Base58EncodedBytes[];
slot: number;
confirmationStatus: string;
err: any;
}[]
};
getInflightBundleStatuses(bundleIds: string[]): {
context: { slot: number };
value: {
bundle_id: string;
status: "Invalid" | "Pending" | "Landed" | "Failed";
landed_slot: number | null;
}[];
};
sendTransaction(transactions: Base64EncodedWireTransaction[]): string;
simulateBundle(transactions: [Base64EncodedWireTransaction[]]): JitoBundleSimulationResponse;
sendBundle(transactions: Base58EncodedBytes[]): string;
}

function createJitoBundlesRpc({ endpoint }: { endpoint: string }): Rpc<LilJitAddon> {
const api = createRpcApi<LilJitAddon>({
// parametersTransformer: (params: any[]) => params[0],
responseTransformer: (response: any) => response.result,
});
const transport = createDefaultRpcTransport({
url: mainnet(endpoint),
});
return createRpc({ api, transport });
}

function isFailedSummary(summary: JitoBundleSimulationResponse['value']['summary']): summary is { failed: any } {
return typeof summary === 'object' && summary !== null && 'failed' in summary;
}

function validateSimulation(simulation: JitoBundleSimulationResponse) {
if (simulation.value.summary !== 'succeeded' && isFailedSummary(simulation.value.summary)) {
throw new Error(`Simulation Failed: ${simulation.value.summary.failed.error.TransactionFailure[1]}`);
}
}


async function createTransaction(
index: number,
latestBlockhash: Parameters<
typeof setTransactionMessageLifetimeUsingBlockhash
>[0],
payerSigner: TransactionPartialSigner,
includeTip?: Address
) {
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(payerSigner, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) =>
appendTransactionMessageInstruction(
getAddMemoInstruction({
memo: `lil jit demo transaction # ${index}`,
}),
tx
),
(tx) =>
includeTip
? appendTransactionMessageInstruction(
getTransferSolInstruction({
source: payerSigner,
destination: includeTip,
amount: MINIMUM_JITO_TIP,
}),
tx
)
: tx
);
return await signTransactionMessageWithSigners(transactionMessage);
}

async function getTipAccount(rpc: Rpc<LilJitAddon>): Promise<Address> {
try {
const tipAccounts = await rpc.getTipAccounts().send();
const jitoTipAddress = tipAccounts[Math.floor(Math.random() * tipAccounts.length)];
if (!jitoTipAddress) {
throw new Error("No JITO tip accounts found");
}
return jitoTipAddress;
} catch {
throw new Error("Failed to get Tip Account");
}
}


async function pollBundleStatus(
rpc: Rpc<LilJitAddon>,
bundleId: string,
timeoutMs = 30000,
pollIntervalMs = 3000
) {
const startTime = Date.now();
let lastStatus = '';
while (Date.now() - startTime < timeoutMs) {
try {
const bundleStatus = await rpc.getInflightBundleStatuses([bundleId]).send();
const status = bundleStatus.value[0]?.status ?? 'Unknown';

if (status !== lastStatus) {
lastStatus = status;
}

if (status === 'Landed') {
return true;
}

if (status === 'Failed') {
console.log(`Bundle ${status.toLowerCase()}. Exiting...`);
throw new Error(`Bundle failed with status: ${status}`);
}

await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
} catch {
console.error('❌ - Error polling bundle status.');
}
}
throw new Error("Polling timeout reached without confirmation");
}



async function main() {
// Step 1 - Setup
const signer = await createKeyPairSignerFromBytes(new Uint8Array(secret));
console.log(`Initializing Jito Bundles demo. Sending ${NUMBER_TRANSACTIONS} transactions from ${signer.address}.`);
const quickNode = {
solana: createSolanaRpc(ENDPOINT),
lilJit: createJitoBundlesRpc({ endpoint: ENDPOINT }),
}
console.log(`✅ - Established connection to QuickNode.`);

// Step 2 - Get a Jitotip account
const jitoTipAddress = await getTipAccount(quickNode.lilJit);
console.log(`✅ - Using the following Jito Tip account: ${jitoTipAddress}`);

// Step 3 - Get Recent Blockhash
const { value: latestBlockhash } = await quickNode.solana
.getLatestBlockhash({ commitment: "confirmed" })
.send();
console.log(`✅ - Latest blockhash: ${latestBlockhash.blockhash}`);

// Step 4 - Create Transactions
const signedTransactions = await Promise.all(
Array.from({ length: NUMBER_TRANSACTIONS }, (_, i) => {
const isLastTransaction = i === NUMBER_TRANSACTIONS - 1;
return createTransaction(
i + 1,
latestBlockhash,
signer,
isLastTransaction ? jitoTipAddress : undefined
);
})
);

const base64EncodedTransactions = signedTransactions.map((transaction) => {
const base64EncodedTransaction = getBase64EncodedWireTransaction(transaction);
return base64EncodedTransaction;
}) as Base64EncodedWireTransaction[];

const transactionEncoder = getTransactionEncoder();
const base58Decoder = getBase58Decoder();

const base58EncodedTransactions = signedTransactions.map((transaction) => {
const transactionBytes = transactionEncoder.encode(transaction);
return base58Decoder.decode(transactionBytes) as Base58EncodedBytes;
});
console.log(`✅ - Trannsactions assembled and encoded.`);

// Step 5 - Simulate Bundle
const simulation = await quickNode.lilJit
.simulateBundle([base64EncodedTransactions])
.send();

validateSimulation(simulation);
console.log(`✅ - Simulation Succeeded.`);

if (SIMULATE_ONLY) {
console.log("🏁 - Simulation Only Mode - Exiting script.");
return;
}

// Step 6 - Send Bundle
let bundleId: string;
try {
bundleId = await quickNode.lilJit
.sendBundle(base58EncodedTransactions)
.send();
console.log(`✅ - Bundle sent: ${bundleId}`);
} catch (error) {
console.error("Error sending bundle:", error);
throw error;
}

// Step 7 - Verify Bundle Landed
await pollBundleStatus(quickNode.lilJit, bundleId, POLL_TIMEOUT_MS, POLL_INTERVAL_MS);
console.log(`✅ - Bundle landed: ${bundleId}`);
console.log(` https://explorer.jito.wtf/bundle/${bundleId}`);
console.log(` (Note: This URL may take a few moments to become available.)`);
}

main().catch((error) => {
console.error(`❌ - Error: ${error}`);
process.exit(1);
});
5 changes: 3 additions & 2 deletions solana/web3.js-2.0/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
"description": "",
"dependencies": {
"@solana-program/compute-budget": "^0.5.0",
"@solana-program/system": "^0.5.0",
"@solana-program/token": "^0.2.1",
"@solana-program/token": "^0.3.2",
"@solana-program/memo": "^0.5.2",
"@solana-program/system": "^0.5.2",
"@solana/web3.js": "^2.0.0-rc.1"
},
"devDependencies": {
Expand Down

0 comments on commit db64d28

Please sign in to comment.