From 4be711525948cf24c0ebd4ebab007dc7f51b7069 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Thu, 7 Nov 2024 22:07:57 +0000 Subject: [PATCH] Refactor/reorg limo examples (#219) * js refactor * python initial refactor * bump versions * address comments --- sdk/js/package-lock.json | 4 +- sdk/js/package.json | 2 +- sdk/js/src/examples/simpleSearcherLimo.ts | 138 ++++++++++++------ .../searcher/examples/simple_searcher_svm.py | 95 ++++++++---- sdk/python/pyproject.toml | 2 +- 5 files changed, 167 insertions(+), 74 deletions(-) diff --git a/sdk/js/package-lock.json b/sdk/js/package-lock.json index b714a29d..b64e025e 100644 --- a/sdk/js/package-lock.json +++ b/sdk/js/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pythnetwork/express-relay-js", - "version": "0.13.2", + "version": "0.13.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pythnetwork/express-relay-js", - "version": "0.13.2", + "version": "0.13.3", "license": "Apache-2.0", "dependencies": { "@coral-xyz/anchor": "^0.30.1", diff --git a/sdk/js/package.json b/sdk/js/package.json index 3c1fb429..95c5ca48 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -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", diff --git a/sdk/js/src/examples/simpleSearcherLimo.ts b/sdk/js/src/examples/simpleSearcherLimo.ts index ab9a2913..88d5ad33 100644 --- a/sdk/js/src/examples/simpleSearcherLimo.ts +++ b/sdk/js/src/examples/simpleSearcherLimo.ts @@ -8,6 +8,7 @@ import { } from "../index"; import { BidStatusUpdate, + BidSvm, ChainId, OpportunityDelete, SvmChainUpdate, @@ -15,17 +16,31 @@ import { 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; @@ -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 { 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 { + 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 { const inputMintDecimals = await this.getMintDecimalsCached( order.state.inputMint ); @@ -128,7 +217,7 @@ class SimpleSearcherLimo { outputAmountDecimals.toString() ); - const ixsTakeOrder = await limoClient.takeOrderIx( + return limoClient.takeOrderIx( this.searcher.publicKey, order, inputAmountDecimals, @@ -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) { @@ -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( diff --git a/sdk/python/express_relay/searcher/examples/simple_searcher_svm.py b/sdk/python/express_relay/searcher/examples/simple_searcher_svm.py index 69da7aa5..36c6bd09 100644 --- a/sdk/python/express_relay/searcher/examples/simple_searcher_svm.py +++ b/sdk/python/express_relay/searcher/examples/simple_searcher_svm.py @@ -3,6 +3,7 @@ import logging import random import typing +from typing import List from decimal import Decimal from solana.rpc.async_api import AsyncClient @@ -10,6 +11,7 @@ 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, @@ -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 @@ -78,6 +86,10 @@ 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()) @@ -85,8 +97,8 @@ async def opportunity_callback(self, opp: Opportunity): 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}" @@ -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( @@ -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, @@ -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 diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 096bda07..3b431c3a 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -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"