Skip to content

Commit

Permalink
Refactor/reorg limo examples (#219)
Browse files Browse the repository at this point in the history
* js refactor

* python initial refactor

* bump versions

* address comments
  • Loading branch information
anihamde authored Nov 7, 2024
1 parent 0c15612 commit 4be7115
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 74 deletions.
4 changes: 2 additions & 2 deletions sdk/js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sdk/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pythnetwork/express-relay-js",
"version": "0.13.2",
"version": "0.13.3",
"description": "Utilities for interacting with the express relay protocol",
"homepage": "https://github.com/pyth-network/per/tree/main/sdk/js",
"author": "Douro Labs",
Expand Down
138 changes: 93 additions & 45 deletions sdk/js/src/examples/simpleSearcherLimo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,39 @@ import {
} from "../index";
import {
BidStatusUpdate,
BidSvm,
ChainId,
OpportunityDelete,
SvmChainUpdate,
} from "../types";
import { SVM_CONSTANTS } from "../const";

import * as anchor from "@coral-xyz/anchor";
import { Keypair, PublicKey, Connection, Blockhash } from "@solana/web3.js";
import {
Keypair,
PublicKey,
Connection,
Blockhash,
TransactionInstruction,
} from "@solana/web3.js";

import * as limo from "@kamino-finance/limo-sdk";
import { Decimal } from "decimal.js";
import {
getMintDecimals,
getPdaAuthority,
OrderStateAndAddress,
} from "@kamino-finance/limo-sdk/dist/utils";

const DAY_IN_SECONDS = 60 * 60 * 24;

type BidData = {
bidAmount: anchor.BN;
router: PublicKey;
relayerSigner: PublicKey;
relayerFeeReceiver: PublicKey;
};

class SimpleSearcherLimo {
private client: Client;
private readonly connectionSvm: Connection;
Expand Down Expand Up @@ -80,12 +95,86 @@ class SimpleSearcherLimo {
return decimals;
}

async generateBid(opportunity: OpportunitySvm, recentBlockhash: Blockhash) {
/**
* Generates a bid for a given opportunity. The transaction in this bid transfers assets from the searcher's wallet to fulfill the limit order.
* @param opportunity The SVM opportunity to bid on
* @returns The generated bid object
*/
async generateBid(opportunity: OpportunitySvm): Promise<BidSvm> {
const order = opportunity.order;
const limoClient = new limo.LimoClient(
this.connectionSvm,
order.state.globalConfig
);

const ixsTakeOrder = await this.generateTakeOrderIxs(limoClient, order);
const txRaw = new anchor.web3.Transaction().add(...ixsTakeOrder);

const bidData = await this.getBidData(limoClient, order);

const bid = await this.client.constructSvmBid(
txRaw,
this.searcher.publicKey,
bidData.router,
order.address,
bidData.bidAmount,
new anchor.BN(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)),
this.chainId,
bidData.relayerSigner,
bidData.relayerFeeReceiver
);

bid.transaction.recentBlockhash = this.recentBlockhash[this.chainId];
bid.transaction.sign(this.searcher);
return bid;
}

/**
* Gets the router address, bid amount, and relayer addresses required to create a valid bid
* @param limoClient The Limo client
* @param order The limit order to be fulfilled
* @returns The fetched bid data
*/
async getBidData(
limoClient: limo.LimoClient,
order: OrderStateAndAddress
): Promise<BidData> {
const router = getPdaAuthority(
limoClient.getProgramID(),
order.state.globalConfig
);
let bidAmount = new anchor.BN(argv.bid);
if (this.bidMargin !== 0) {
const margin = new anchor.BN(
Math.floor(Math.random() * (this.bidMargin * 2 + 1)) - this.bidMargin
);
bidAmount = bidAmount.add(margin);
}
if (!this.expressRelayConfig) {
this.expressRelayConfig = await this.client.getExpressRelaySvmConfig(
this.chainId,
this.connectionSvm
);
}

return {
bidAmount,
router,
relayerSigner: this.expressRelayConfig.relayerSigner,
relayerFeeReceiver: this.expressRelayConfig.feeReceiverRelayer,
};
}

/**
* Creates the take order instructions on the Limo program
* @param limoClient The Limo client
* @param order The limit order to be fulfilled
* @returns The Limo TakeOrder instructions used to fulfill the order
*/
async generateTakeOrderIxs(
limoClient: limo.LimoClient,
order: OrderStateAndAddress
): Promise<TransactionInstruction[]> {
const inputMintDecimals = await this.getMintDecimalsCached(
order.state.inputMint
);
Expand Down Expand Up @@ -128,7 +217,7 @@ class SimpleSearcherLimo {
outputAmountDecimals.toString()
);

const ixsTakeOrder = await limoClient.takeOrderIx(
return limoClient.takeOrderIx(
this.searcher.publicKey,
order,
inputAmountDecimals,
Expand All @@ -137,43 +226,6 @@ class SimpleSearcherLimo {
inputMintDecimals,
outputMintDecimals
);
const txRaw = new anchor.web3.Transaction().add(...ixsTakeOrder);

const router = getPdaAuthority(
limoClient.getProgramID(),
order.state.globalConfig
);

let bidAmount = new anchor.BN(argv.bid);
if (this.bidMargin !== 0) {
const margin = new anchor.BN(
Math.floor(Math.random() * (this.bidMargin * 2 + 1)) - this.bidMargin
);
bidAmount = bidAmount.add(margin);
}

if (!this.expressRelayConfig) {
this.expressRelayConfig = await this.client.getExpressRelaySvmConfig(
this.chainId,
this.connectionSvm
);
}

const bid = await this.client.constructSvmBid(
txRaw,
this.searcher.publicKey,
router,
order.address,
bidAmount,
new anchor.BN(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)),
this.chainId,
this.expressRelayConfig.relayerSigner,
this.expressRelayConfig.feeReceiverRelayer
);

bid.transaction.recentBlockhash = recentBlockhash;
bid.transaction.sign(this.searcher);
return bid;
}

async opportunityHandler(opportunity: Opportunity) {
Expand All @@ -189,11 +241,7 @@ class SimpleSearcherLimo {
console.log(`Adding latency of ${latency}ms`);
await new Promise((resolve) => setTimeout(resolve, latency));
}

const bid = await this.generateBid(
opportunity as OpportunitySvm,
this.recentBlockhash[this.chainId]
);
const bid = await this.generateBid(opportunity as OpportunitySvm);
try {
const bidId = await this.client.submitBid(bid);
console.log(
Expand Down
95 changes: 70 additions & 25 deletions sdk/python/express_relay/searcher/examples/simple_searcher_svm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import logging
import random
import typing
from typing import List
from decimal import Decimal

from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Finalized
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.transaction import Transaction
from solders.instruction import Instruction

from express_relay.client import (
ExpressRelayClient,
Expand All @@ -31,6 +33,12 @@
DEADLINE = 2**62
logger = logging.getLogger(__name__)

class BidData:
def __init__(self, router: Pubkey, bid_amount: int, relayer_signer: Pubkey, relayer_fee_receiver: Pubkey):
self.router = router
self.bid_amount = bid_amount
self.relayer_signer = relayer_signer
self.relayer_fee_receiver = relayer_fee_receiver

class SimpleSearcherSvm:
express_relay_metadata: ExpressRelayMetadata | None
Expand Down Expand Up @@ -78,15 +86,19 @@ async def opportunity_callback(self, opp: Opportunity):
Args:
opp: An object representing a single opportunity.
"""
if opp.chain_id not in self.recent_blockhash:
logger.info(f"No recent blockhash for chain, {opp.chain_id} skipping bid")
return None

if self.with_latency:
await asyncio.sleep(0.5 * random.random())

bid = await self.assess_opportunity(typing.cast(OpportunitySvm, opp))

if bid:
try:
await self.client.submit_bid(bid)
logger.info(f"Submitted bid for opportunity {str(opp.opportunity_id)}")
bid_id = await self.client.submit_bid(bid)
logger.info(f"Submitted bid {str(bid_id)} for opportunity {str(opp.opportunity_id)}")
except Exception as e:
logger.error(
f"Error submitting bid for opportunity {str(opp.opportunity_id)}: {e}"
Expand Down Expand Up @@ -119,7 +131,46 @@ async def get_mint_decimals(self, mint: Pubkey) -> int:
return self.mint_decimals_cache[str(mint)]

async def assess_opportunity(self, opp: OpportunitySvm) -> BidSvm | None:
"""
Method to assess an opportunity and return a bid if the opportunity is worth taking. This method always returns a bid for any valid opportunity. The transaction in this bid transfers assets from the searcher's wallet to fulfill the limit order.
Args:
opp: An object representing a single opportunity.
Returns:
A bid object if the opportunity is worth taking to be submitted to the Express Relay server, otherwise None.
"""
order: OrderStateAndAddress = {"address": opp.order_address, "state": opp.order}
ixs_take_order = await self.generate_take_order_ixs(order)
bid_data = await self.get_bid_data(order)

submit_bid_ix = self.client.get_svm_submit_bid_instruction(
searcher=self.private_key.pubkey(),
router=bid_data.router,
permission_key=order["address"],
bid_amount=bid_data.bid_amount,
deadline=DEADLINE,
chain_id=self.chain_id,
fee_receiver_relayer=bid_data.relayer_fee_receiver,
relayer_signer=bid_data.relayer_signer,
)
transaction = Transaction.new_with_payer(
[submit_bid_ix] + ixs_take_order, self.private_key.pubkey()
)
transaction.partial_sign(
[self.private_key], recent_blockhash=self.recent_blockhash[self.chain_id]
)
bid = BidSvm(transaction=transaction, chain_id=self.chain_id)
return bid

async def generate_take_order_ixs(self, order: OrderStateAndAddress) -> List[Instruction]:
"""
Helper method to form the Limo instructions to take an order.
Args:
order: An object representing the order to be fulfilled.
Returns:
A list of Limo instructions to take an order.
"""
input_mint_decimals = await self.get_mint_decimals(order["state"].input_mint)
output_mint_decimals = await self.get_mint_decimals(order["state"].output_mint)
effective_fill_rate = min(
Expand Down Expand Up @@ -151,10 +202,25 @@ async def assess_opportunity(self, opp: OpportunitySvm) -> BidSvm | None:
output_mint_decimals,
self.svm_config["express_relay_program"],
)
return ixs_take_order

async def get_bid_data(self, order: OrderStateAndAddress) -> BidData:
"""
Helper method to get the bid data for an opportunity.
Args:
order: An object representing the order to be fulfilled.
Returns:
A BidData object representing the bid data for the opportunity. Consists of the router pubkey, bid amount, relayer signer pubkey, and relayer fee receiver pubkey.
"""
router = self.limo_client.get_pda_authority(
self.limo_client.get_program_id(), order["state"].global_config
)

bid_amount = self.bid_amount
if self.bid_margin != 0:
bid_amount += random.randint(-self.bid_margin, self.bid_margin)

if self.express_relay_metadata is None:
self.express_relay_metadata = await ExpressRelayMetadata.fetch(
self.rpc_client,
Expand All @@ -166,33 +232,12 @@ async def assess_opportunity(self, opp: OpportunitySvm) -> BidSvm | None:
if self.express_relay_metadata is None:
raise ValueError("Express relay metadata account not found")

bid_amount = self.bid_amount
if self.bid_margin != 0:
bid_amount += random.randint(-self.bid_margin, self.bid_margin)

submit_bid_ix = self.client.get_svm_submit_bid_instruction(
searcher=self.private_key.pubkey(),
return BidData(
router=router,
permission_key=order["address"],
bid_amount=bid_amount,
deadline=DEADLINE,
chain_id=self.chain_id,
fee_receiver_relayer=self.express_relay_metadata.fee_receiver_relayer,
relayer_signer=self.express_relay_metadata.relayer_signer,
relayer_fee_receiver=self.express_relay_metadata.fee_receiver_relayer
)
transaction = Transaction.new_with_payer(
[submit_bid_ix] + ixs_take_order, self.private_key.pubkey()
)

if opp.chain_id not in self.recent_blockhash:
logger.info(f"No recent blockhash for chain, {opp.chain_id} skipping bid")
return None

transaction.partial_sign(
[self.private_key], recent_blockhash=self.recent_blockhash[self.chain_id]
)
bid = BidSvm(transaction=transaction, chain_id=self.chain_id)
return bid

async def svm_chain_update_callback(self, svm_chain_update: SvmChainUpdate):
self.recent_blockhash[svm_chain_update.chain_id] = svm_chain_update.blockhash
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "express-relay"
version = "0.13.1"
version = "0.13.3"
description = "Utilities for searchers and protocols to interact with the Express Relay protocol."
authors = ["dourolabs"]
license = "Apache-2.0"
Expand Down

0 comments on commit 4be7115

Please sign in to comment.