Skip to content

Commit

Permalink
Implement jito bundle for fulfill package solana (#8)
Browse files Browse the repository at this point in the history
Co-authored-by: mrlotfi <[email protected]>
  • Loading branch information
mrlotfi and mrlotfi authored Sep 1, 2024
1 parent b30f903 commit 817f580
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 112 deletions.
2 changes: 1 addition & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ POLYGON_RPC="https://polygon-rpc.com/"
DISABLE_UNLOCKER="false"
BLACKLISTED_REFERRERS=""

SOLANA_TX_MODE="NORMAL" # [NORMAL | JITO | BOTH]
SOLANA_TX_MODE="NORMAL" # [NORMAL | JITO]
JITO_ENDPOINT="https://frankfurt.mainnet.block-engine.jito.wtf"
MIN_JITO_TIP="0.0001"
MAX_JITO_TIP="0.0002"
5 changes: 2 additions & 3 deletions src/config/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type RpcConfig = {
solana: {
fulfillTxMode: 'NORMAL' | 'JITO' | 'BOTH';
fulfillTxMode: 'NORMAL' | 'JITO';
jitoEndpoint: string;
sendCount: number;
solanaMainRpc: string;
Expand All @@ -27,8 +27,7 @@ export type RpcConfig = {

export const rpcConfig: RpcConfig = {
solana: {
fulfillTxMode:
process.env.SOLANA_TX_MODE === 'JITO' ? 'JITO' : process.env.SOLANA_TX_MODE === 'BOTH' ? 'BOTH' : 'NORMAL',
fulfillTxMode: process.env.SOLANA_TX_MODE === 'JITO' ? 'JITO' : 'NORMAL',
jitoEndpoint: process.env.JITO_ENDPOINT || 'https://frankfurt.mainnet.block-engine.jito.wtf',
otherSendInterval: 5000,
sendInterval: 1000,
Expand Down
52 changes: 43 additions & 9 deletions src/driver/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,12 +547,12 @@ export class DriverService {
}

logger.info(`Sending fulfill transaction for ${swap.sourceTxHash}`);
const hash = await this.solanaSender.createAndSendTransactionJitoAndNormal(
const hash = await this.solanaSender.createAndSendOptimizedTransaction(
trxData.instructions,
trxData.signers,
trxData.lookupTables,
true,
this.rpcConfig.solana.sendCount,
true,
);
logger.info(`Sent fulfill transaction for ${swap.sourceTxHash} with ${hash}`);
} else {
Expand Down Expand Up @@ -581,12 +581,12 @@ export class DriverService {
let instructions = [registerOrderIx, ...fulfillIxs, ...settleIxs];

logger.info(`Sending noacution settle transaction for ${swap.sourceTxHash}`);
const hash = await this.solanaSender.createAndSendTransactionJitoAndNormal(
const hash = await this.solanaSender.createAndSendOptimizedTransaction(
instructions,
[this.walletConfig.solana],
[],
true,
this.rpcConfig.solana.sendCount,
true,
);
logger.info(`Sent noauction settle transaction for ${swap.sourceTxHash} with ${hash}`);
return hash;
Expand Down Expand Up @@ -716,27 +716,61 @@ export class DriverService {

async auctionFulfillAndSettlePackage(swap: Swap) {
logger.info(`Getting swapless fulfill-settle package for ${swap.sourceTxHash}`);
const postBidData = await this.postBid(swap, true, false, true);
const fulfillData = await this.fulfill(swap, undefined, true);
const settleData = await this.settle(swap, true);
const [postBidData, fulfillData, settleData] = await Promise.all([
this.postBid(swap, true, false, true),
this.fulfill(swap, undefined, true),
this.settle(swap, true),
]);

let finalInstructions: TransactionInstruction[] = [];
finalInstructions.push(...postBidData!.instructions!);
finalInstructions.push(...fulfillData!.instructions!);
finalInstructions.push(...settleData!.instructions!);
logger.info(`Sending fulfill-settle package for ${swap.sourceTxHash}`);
const hash = await this.solanaSender.createAndSendTransactionJitoAndNormal(
const hash = await this.solanaSender.createAndSendOptimizedTransaction(
finalInstructions,
[this.walletConfig.solana, ...postBidData?.signers!, ...fulfillData?.signers!],
fulfillData!.lookupTables,
true,
this.rpcConfig.solana.sendCount,
true,
undefined,
200_000,
);
logger.info(`Sent fulfill-settle package for ${swap.sourceTxHash} with ${hash}`);
}

async auctionFulfillAndSettleJitoBundle(swap: Swap) {
logger.info(`Getting jito fulfill-settle package for ${swap.sourceTxHash}`);
const [postBidData, fulfillData, settleData] = await Promise.all([
this.postBid(swap, true, false, true),
this.fulfill(swap, undefined, true),
this.settle(swap, true),
]);

logger.info(`Sending jito fulfill-settle package for ${swap.sourceTxHash}`);
await this.solanaSender.createAndSendJitoBundle(
[
{
instructions: postBidData!.instructions!,
signers: postBidData!.signers!,
lookupTables: [],
},
{
instructions: fulfillData!.instructions!,
signers: fulfillData!.signers!,
lookupTables: fulfillData!.lookupTables,
},
{
instructions: [...settleData!.instructions!],
signers: [],
lookupTables: [],
},
],
4,
);
logger.info(`Sent jito fulfill-settle package for ${swap.sourceTxHash}`);
}

async submitGaslessOrder(swap: Swap) {
await this.evmFulFiller.submitGaslessOrder(swap);
}
Expand Down
41 changes: 24 additions & 17 deletions src/relayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,14 @@ export class Relayer {
) {}

private async tryProgressFulfill(swap: Swap) {
let sourceState: SwiftSourceState | null = null;
let destState: SwiftDestState | null = null;
let destEvmOrder: EvmStoredOrder | null = null;
let sourceEvmOrder: EvmStoredOrder | null = null;

if (swap.destChain === CHAIN_ID_SOLANA) {
destState = await getSwiftStateDest(this.solanaConnection, new PublicKey(swap.stateAddr));
} else {
destEvmOrder = await this.walletHelper.getReadContract(swap.destChain).orders(swap.orderHash);
}
if (swap.sourceChain === CHAIN_ID_SOLANA) {
sourceState = await getSwiftStateSrc(this.solanaConnection, new PublicKey(swap.stateAddr));
} else {
sourceEvmOrder = await this.walletHelper.getReadContract(swap.sourceChain).orders(swap.orderHash);
}
const isSolDst = swap.destChain === CHAIN_ID_SOLANA;
const isSolSrc = swap.sourceChain === CHAIN_ID_SOLANA;
let [destState, destEvmOrder, sourceState, sourceEvmOrder] = await Promise.all([
isSolDst ? getSwiftStateDest(this.solanaConnection, new PublicKey(swap.stateAddr)) : null,
!isSolDst ? this.walletHelper.getReadContract(swap.destChain).orders(swap.orderHash) : null,
isSolSrc ? getSwiftStateSrc(this.solanaConnection, new PublicKey(swap.stateAddr)) : null,
!isSolSrc ? this.walletHelper.getReadContract(swap.sourceChain).orders(swap.orderHash) : null,
]);

switch (swap.status) {
case SWAP_STATUS.ORDER_SUBMITTED:
Expand Down Expand Up @@ -317,8 +310,10 @@ export class Relayer {
return;
}

const solanaTime = await getCurrentSolanaTimeMS(this.solanaConnection);
let auctionState = await getAuctionState(this.solanaConnection, new PublicKey(swap.auctionStateAddr));
let [solanaTime, auctionState] = await Promise.all([
getCurrentSolanaTimeMS(this.solanaConnection),
getAuctionState(this.solanaConnection, new PublicKey(swap.auctionStateAddr)),
]);
if (!!auctionState && auctionState.winner !== this.walletConfig.solana.publicKey.toString()) {
const openToBid = this.isAuctionOpenToBid(auctionState, solanaTime);
if (!openToBid) {
Expand Down Expand Up @@ -354,6 +349,18 @@ export class Relayer {
await this.driverService.auctionFulfillAndSettlePackage(swap);
swap.status = SWAP_STATUS.ORDER_SETTLED;
} else {
if (this.rpcConfig.solana.fulfillTxMode === 'JITO') {
try {
// send everything as bundle. If we fail to land under like 10 seconds, fall back to sending txs separately
await this.driverService.auctionFulfillAndSettleJitoBundle(swap);
swap.status = SWAP_STATUS.ORDER_SETTLED;
return;
} catch (e: any) {
logger.warn(
`Failed to send bundle for ${swap.sourceTxHash}. Falling back to sending each tx separately. errors: ${e} ${e.stack}`,
);
}
}
await this.driverService.postBid(swap, true, false);
await this.waitForFinalizeOnSource(swap);

Expand Down
125 changes: 43 additions & 82 deletions src/utils/solana-trx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
AddressLookupTableAccount,
ComputeBudgetProgram,
Connection,
MessageV0,
PublicKey,
Signer,
SystemProgram,
Expand Down Expand Up @@ -76,115 +77,74 @@ export class SolanaMultiTxSender {
}
}

async createAndSendTransactionJitoAndNormal(
instructions: TransactionInstruction[],
signers: Signer[],
lookupTables: AddressLookupTableAccount[],
addPriorityFeeIns: boolean,
sendCounts: number,
feePayer?: Signer,
manualComputeUnits?: number,
): Promise<string> {
let promises: Promise<string>[] = [];

if (['JITO', 'BOTH'].includes(this.rpcConfig.solana.fulfillTxMode)) {
let newInstructions = [];
for (let ins of instructions) {
newInstructions.push(ins);
}
const jitoResult = this.createAndSendWithJito(
newInstructions,
signers,
lookupTables,
addPriorityFeeIns,
feePayer,
);
promises.push(jitoResult);
}

if (['NORMAL', 'BOTH'].includes(this.rpcConfig.solana.fulfillTxMode)) {
let newInstructions = [];
for (let ins of instructions) {
newInstructions.push(ins);
}
const normalResult = this.createAndSendOptimizedTransaction(
newInstructions,
signers,
lookupTables,
sendCounts,
addPriorityFeeIns,
feePayer,
manualComputeUnits,
);
promises.push(normalResult);
}
getRandomJitoTransferIx(): TransactionInstruction {
const ix = SystemProgram.transfer({
fromPubkey: this.walletConfig.solana.publicKey,
toPubkey: this.chooseJitoTipAccount(),
lamports: Math.floor(this.minJitoTipAmount * 10 ** 9),
});
return ix;
}

const results = await Promise.allSettled(promises);
for (let res of results) {
if (res.status === 'fulfilled') {
return res.value;
}
async createAndSendJitoBundle(
txDatas: {
instructions: TransactionInstruction[];
signers: Signer[];
lookupTables: AddressLookupTableAccount[];
}[],
timeoutSeconds: number,
): Promise<string> {
if (txDatas.length > 5) {
throw new Error('Cannot send more than 5 transactions in a single bundle');
}

for (let res of results) {
if (res.status === 'rejected') {
logger.error(`Error sending transaction: ${res.reason}`);
let txs: string[] = [];
let { blockhash: recentBlockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash();
for (let i = 0; i < txDatas.length; i++) {
const txData = txDatas[i];
let instructions = txData.instructions;
if (i === txDatas.length - 1) {
instructions.push(this.getRandomJitoTransferIx());
}
const msg = MessageV0.compile({
payerKey: this.walletConfig.solana.publicKey,
instructions: instructions,
addressLookupTableAccounts: txData.lookupTables,
recentBlockhash,
});
const trx = new VersionedTransaction(msg);
trx.sign([this.walletConfig.solana, ...txData.signers]);
const trxBS58 = binary_to_base58(trx.serialize());
txs.push(trxBS58);
}

throw new Error('Both jito and normal send tx failed');
}

async createAndSendWithJito(
instructions: TransactionInstruction[],
signers: Signer[],
lookupTables: AddressLookupTableAccount[],
addPriorityFeeIns: boolean = true,
feePayer?: Signer,
): Promise<string> {
instructions.unshift(
SystemProgram.transfer({
fromPubkey: this.walletConfig.solana.publicKey,
toPubkey: this.chooseJitoTipAccount(),
lamports: Math.floor(this.minJitoTipAmount * 10 ** 9),
}),
);
const { trx, lastValidBlockheight } = await this.createOptimizedVersionedTransaction(
instructions,
signers,
lookupTables,
addPriorityFeeIns,
feePayer,
);
const rawTrx = trx.serialize();

logger.info(`Posting ${txs.length} transactions to jito`);
const res = await axios.post(
`${this.rpcConfig.solana.jitoEndpoint}/api/v1/bundles`,
{
jsonrpc: '2.0',
id: 1,
method: 'sendBundle',
params: [[binary_to_base58(rawTrx)]],
params: [txs],
},
{
headers: { 'Content-Type': 'application/json' },
},
);
const bundleId = res.data.result;

const timeout = 60000; // 30 second timeout
const interval = 3000; // 3 second interval
const timeout = timeoutSeconds * 1000;
const interval = 1000;
const startTime = Date.now();

while (Date.now() - startTime < timeout || (await this.connection.getBlockHeight()) <= lastValidBlockheight) {
while (Date.now() - startTime < timeout || (await this.connection.getBlockHeight()) <= lastValidBlockHeight) {
const bundleStatuses = await getBundleStatuses(
[bundleId],
`${this.rpcConfig.solana.jitoEndpoint}/api/v1/bundles`,
);

if (bundleStatuses && bundleStatuses.value && bundleStatuses.value.length > 0) {
const status = bundleStatuses.value[0].confirmation_status;

if (status === 'confirmed' || status === 'finalized') {
const txHash = bundleStatuses.value[0].transactions[0];
const tx = await this.connection.getSignatureStatus(txHash);
Expand All @@ -195,10 +155,11 @@ export class SolanaMultiTxSender {
throw new Error(`Bundle failed with error: ${tx.value.err}`);
}

logger.info(`Posted ${status} transactions to jito with ${bundleId}`);

return txHash;
}
}

await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error('Bundle failed to confirm within the timeout period');
Expand Down

0 comments on commit 817f580

Please sign in to comment.