diff --git a/Tiltfile b/Tiltfile index 56d9e83d..73792454 100644 --- a/Tiltfile +++ b/Tiltfile @@ -222,14 +222,14 @@ local_resource( local_resource( "svm-searcher-py", - serve_cmd="poetry run python3 -m express_relay.searcher.examples.simple_searcher_svm --endpoint-express-relay http://127.0.0.1:9000 --chain-id development-solana --private-key-json-file ../../keypairs/searcher_js.json --endpoint-svm http://127.0.0.1:8899 --bid 10000000 --fill-rate 4 --bid-margin 1 --with-latency", + serve_cmd="poetry run python3 -m express_relay.searcher.examples.testing_searcher_svm --endpoint-express-relay http://127.0.0.1:9000 --chain-id development-solana --private-key-json-file ../../keypairs/searcher_js.json --endpoint-svm http://127.0.0.1:8899 --bid 10000000 --fill-rate 4 --bid-margin 100 --with-latency", serve_dir="sdk/python", resource_deps=["svm-initialize-programs", "auction-server"], ) local_resource( "svm-searcher-js", - serve_cmd="npm run simple-searcher-limo -- --endpoint-express-relay http://127.0.0.1:9000 --chain-id development-solana --private-key-json-file ../../keypairs/searcher_py.json --endpoint-svm http://127.0.0.1:8899 --bid 10000000 --fill-rate 4 --bid-margin 1 --with-latency", + serve_cmd="npm run testing-searcher-limo -- --endpoint-express-relay http://127.0.0.1:9000 --chain-id development-solana --private-key-json-file ../../keypairs/searcher_py.json --endpoint-svm http://127.0.0.1:8899 --bid 10000000 --fill-rate 4 --bid-margin 100 --with-latency", serve_dir="sdk/js", resource_deps=["svm-initialize-programs", "auction-server"], ) diff --git a/sdk/js/package-lock.json b/sdk/js/package-lock.json index b64e025e..d7a12fc8 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.3", + "version": "0.13.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pythnetwork/express-relay-js", - "version": "0.13.3", + "version": "0.13.4", "license": "Apache-2.0", "dependencies": { "@coral-xyz/anchor": "^0.30.1", diff --git a/sdk/js/package.json b/sdk/js/package.json index 95c5ca48..1d7c686c 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/express-relay-js", - "version": "0.13.3", + "version": "0.13.4", "description": "Utilities for interacting with the express relay protocol", "homepage": "https://github.com/pyth-network/per/tree/main/sdk/js", "author": "Douro Labs", @@ -16,8 +16,8 @@ "build": "tsc", "test": "jest src/ --passWithNoTests", "simple-searcher-evm": "npm run build && node lib/examples/simpleSearcherEvm.js", - "simple-searcher-svm": "npm run build && node lib/examples/simpleSearcherSvm.js", "simple-searcher-limo": "npm run build && node lib/examples/simpleSearcherLimo.js", + "testing-searcher-limo": "npm run build && node lib/examples/testingSearcherLimo.js", "generate-api-types": "openapi-typescript http://127.0.0.1:9000/docs/openapi.json --output src/serverTypes.d.ts", "generate-anchor-types": "anchor idl type src/idl/idlExpressRelay.json --out src/expressRelayTypes.d.ts && anchor idl type src/examples/idl/idlDummy.json --out src/examples/dummyTypes.d.ts", "format": "prettier --write \"src/**/*.ts\"", diff --git a/sdk/js/src/examples/simpleSearcherLimo.ts b/sdk/js/src/examples/simpleSearcherLimo.ts index 88d5ad33..cfcd0e27 100644 --- a/sdk/js/src/examples/simpleSearcherLimo.ts +++ b/sdk/js/src/examples/simpleSearcherLimo.ts @@ -34,27 +34,19 @@ import { 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; - private mintDecimals: Record = {}; - private expressRelayConfig: ExpressRelaySvmConfig | undefined; - private recentBlockhash: Record = {}; +export class SimpleSearcherLimo { + protected client: Client; + protected readonly connectionSvm: Connection; + protected mintDecimals: Record = {}; + protected expressRelayConfig: ExpressRelaySvmConfig | undefined; + protected recentBlockhash: Record = {}; + protected readonly bid: anchor.BN; constructor( public endpointExpressRelay: string, public chainId: string, - private searcher: Keypair, + protected searcher: Keypair, public endpointSvm: string, - public fillRate: number, - public withLatency: boolean, - public bidMargin: number, + bid: number, public apiKey?: string ) { this.client = new Client( @@ -68,6 +60,7 @@ class SimpleSearcherLimo { this.svmChainUpdateHandler.bind(this), this.removeOpportunitiesHandler.bind(this) ); + this.bid = new anchor.BN(bid); this.connectionSvm = new Connection(endpointSvm, "confirmed"); } @@ -110,18 +103,19 @@ class SimpleSearcherLimo { const ixsTakeOrder = await this.generateTakeOrderIxs(limoClient, order); const txRaw = new anchor.web3.Transaction().add(...ixsTakeOrder); - const bidData = await this.getBidData(limoClient, order); + const bidAmount = await this.getBidAmount(order); + const config = await this.getExpressRelayConfig(); const bid = await this.client.constructSvmBid( txRaw, this.searcher.publicKey, - bidData.router, + getPdaAuthority(limoClient.getProgramID(), order.state.globalConfig), order.address, - bidData.bidAmount, + bidAmount, new anchor.BN(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)), this.chainId, - bidData.relayerSigner, - bidData.relayerFeeReceiver + config.relayerSigner, + config.feeReceiverRelayer ); bid.transaction.recentBlockhash = this.recentBlockhash[this.chainId]; @@ -129,40 +123,25 @@ class SimpleSearcherLimo { 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); - } + async getExpressRelayConfig(): Promise { if (!this.expressRelayConfig) { this.expressRelayConfig = await this.client.getExpressRelaySvmConfig( this.chainId, this.connectionSvm ); } + return this.expressRelayConfig; + } - return { - bidAmount, - router, - relayerSigner: this.expressRelayConfig.relayerSigner, - relayerFeeReceiver: this.expressRelayConfig.feeReceiverRelayer, - }; + /** + * Calculates the bid amount for a given order. + * @param order The limit order to be fulfilled + * @returns The bid amount in lamports + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getBidAmount(order: OrderStateAndAddress): Promise { + // this should be replaced by a more sophisticated logic to determine the bid amount + return this.bid; } /** @@ -181,16 +160,11 @@ class SimpleSearcherLimo { const outputMintDecimals = await this.getMintDecimalsCached( order.state.outputMint ); - const effectiveFillRate = Math.min( - this.fillRate, - (100 * order.state.remainingInputAmount.toNumber()) / - order.state.initialInputAmount.toNumber() - ); + const effectiveFillRate = this.getEffectiveFillRate(order); const inputAmountDecimals = new Decimal( order.state.initialInputAmount.toNumber() ) .mul(effectiveFillRate) - .div(100) .floor() .div(new Decimal(10).pow(inputMintDecimals)); @@ -198,7 +172,6 @@ class SimpleSearcherLimo { order.state.expectedOutputAmount.toNumber() ) .mul(effectiveFillRate) - .div(100) .ceil() .div(new Decimal(10).pow(outputMintDecimals)); @@ -228,6 +201,12 @@ class SimpleSearcherLimo { ); } + protected getEffectiveFillRate(order: OrderStateAndAddress): Decimal { + return new Decimal(order.state.remainingInputAmount.toNumber()).div( + new Decimal(order.state.initialInputAmount.toNumber()) + ); + } + async opportunityHandler(opportunity: Opportunity) { if (!this.recentBlockhash[this.chainId]) { console.log( @@ -235,12 +214,6 @@ class SimpleSearcherLimo { ); return; } - - if (this.withLatency) { - const latency = Math.floor(Math.random() * 500); - console.log(`Adding latency of ${latency}ms`); - await new Promise((resolve) => setTimeout(resolve, latency)); - } const bid = await this.generateBid(opportunity as OpportunitySvm); try { const bidId = await this.client.submitBid(bid); @@ -266,10 +239,11 @@ class SimpleSearcherLimo { } async start() { + console.log(`Using searcher pubkey: ${this.searcher.publicKey.toBase58()}`); try { - await this.client.subscribeChains([argv.chainId]); + await this.client.subscribeChains([this.chainId]); console.log( - `Subscribed to chain ${argv.chainId}. Waiting for opportunities...` + `Subscribed to chain ${this.chainId}. Waiting for opportunities...` ); } catch (error) { console.error(error); @@ -278,100 +252,88 @@ class SimpleSearcherLimo { } } -const argv = yargs(hideBin(process.argv)) - .option("endpoint-express-relay", { - description: - "Express relay endpoint. e.g: https://per-staging.dourolabs.app/", - type: "string", - demandOption: true, - }) - .option("chain-id", { - description: "Chain id to bid on Limo opportunities for. e.g: solana", - type: "string", - demandOption: true, - }) - .option("bid", { - description: "Bid amount in lamports", - type: "string", - default: "100", - }) - .option("private-key", { - description: "Private key of the searcher in base58 format", - type: "string", - conflicts: "private-key-json-file", - }) - .option("private-key-json-file", { - description: - "Path to a json file containing the private key of the searcher in array of bytes format", - type: "string", - conflicts: "private-key", - }) - .option("api-key", { - description: - "The API key of the searcher to authenticate with the server for fetching and submitting bids", - type: "string", - demandOption: false, - }) - .option("endpoint-svm", { - description: "SVM RPC endpoint", - type: "string", - demandOption: true, - }) - .option("fill-rate", { - description: - "How much of the initial order size to fill in percentage. Default is 100%", - type: "number", - default: 100, - }) - .option("with-latency", { - description: - "Whether to add random latency to the bid submission. Default is false", - type: "boolean", - default: false, - }) - .option("bid-margin", { - description: - "The margin to add or subtract from the bid. For example, 1 means the bid range is [bid - 1, bid + 1]. Default is 0", - type: "number", - default: 0, - }) - .help() - .alias("help", "h") - .parseSync(); -async function run() { - if (!SVM_CONSTANTS[argv.chainId]) { - throw new Error(`SVM constants not found for chain ${argv.chainId}`); - } - let searcherKeyPair; +export function makeParser() { + return yargs(hideBin(process.argv)) + .option("endpoint-express-relay", { + description: + "Express relay endpoint. e.g: https://per-staging.dourolabs.app/", + type: "string", + demandOption: true, + }) + .option("chain-id", { + description: "Chain id to bid on Limo opportunities for. e.g: solana", + type: "string", + demandOption: true, + choices: Object.keys(SVM_CONSTANTS), + }) + .option("bid", { + description: "Bid amount in lamports", + type: "number", + default: 100, + }) + .option("private-key", { + description: "Private key of the searcher in base58 format", + type: "string", + conflicts: "private-key-json-file", + }) + .option("private-key-json-file", { + description: + "Path to a json file containing the private key of the searcher in array of bytes format", + type: "string", + conflicts: "private-key", + }) + .option("api-key", { + description: + "The API key of the searcher to authenticate with the server for fetching and submitting bids", + type: "string", + demandOption: false, + }) + .option("endpoint-svm", { + description: "SVM RPC endpoint", + type: "string", + demandOption: true, + }) + .help() + .alias("help", "h"); +} - if (argv.privateKey) { - const secretKey = anchor.utils.bytes.bs58.decode(argv.privateKey); - searcherKeyPair = Keypair.fromSecretKey(secretKey); - } else if (argv.privateKeyJsonFile) { - searcherKeyPair = Keypair.fromSecretKey( - Buffer.from( - // eslint-disable-next-line @typescript-eslint/no-var-requires - JSON.parse(require("fs").readFileSync(argv.privateKeyJsonFile)) - ) - ); +export function getKeypair( + privateKey: string | undefined, + privateKeyJsonFile: string | undefined +): Keypair { + if (privateKey) { + const secretKey = anchor.utils.bytes.bs58.decode(privateKey); + return Keypair.fromSecretKey(secretKey); } else { - throw new Error( - "Either private-key or private-key-json-file must be provided" - ); + if (privateKeyJsonFile) { + return Keypair.fromSecretKey( + Buffer.from( + // eslint-disable-next-line @typescript-eslint/no-var-requires + JSON.parse(require("fs").readFileSync(privateKeyJsonFile)) + ) + ); + } else { + throw new Error( + "Either private-key or private-key-json-file must be provided" + ); + } } - console.log(`Using searcher pubkey: ${searcherKeyPair.publicKey.toBase58()}`); +} +async function run() { + const argv = makeParser().parseSync(); + const searcherKeyPair = getKeypair(argv.privateKey, argv.privateKeyJsonFile); const simpleSearcher = new SimpleSearcherLimo( argv.endpointExpressRelay, argv.chainId, searcherKeyPair, argv.endpointSvm, - argv.fillRate, - argv.withLatency, - argv.bidMargin, + new anchor.BN(argv.bid), argv.apiKey ); await simpleSearcher.start(); } -run(); +if (require.main === module) { + run(); +} diff --git a/sdk/js/src/examples/simpleSearcherSvm.ts b/sdk/js/src/examples/simpleSearcherSvm.ts deleted file mode 100644 index 9f9c995c..00000000 --- a/sdk/js/src/examples/simpleSearcherSvm.ts +++ /dev/null @@ -1,195 +0,0 @@ -import yargs from "yargs"; -import { hideBin } from "yargs/helpers"; -import { Client } from "../index"; -import { BidStatusUpdate } from "../types"; -import { SVM_CONSTANTS } from "../const"; - -import * as anchor from "@coral-xyz/anchor"; -import { Program, AnchorProvider } from "@coral-xyz/anchor"; -import { Keypair, PublicKey, Connection } from "@solana/web3.js"; -import dummyIdl from "./idl/idlDummy.json"; -import { Dummy } from "./dummyTypes"; -import { getConfigRouterPda, getExpressRelayMetadataPda } from "../svm"; - -const DAY_IN_SECONDS = 60 * 60 * 24; -const DUMMY_PIDS: Record = { - "development-solana": new PublicKey( - "HYCgALnu6CM2gkQVopa1HGaNf8Vzbs9bomWRiKP267P3" - ), -}; - -class SimpleSearcherSvm { - private client: Client; - private connectionSvm: Connection; - constructor( - public endpointExpressRelay: string, - public chainId: string, - public privateKey: string, - public endpointSvm: string, - public apiKey?: string - ) { - this.client = new Client( - { - baseUrl: endpointExpressRelay, - apiKey, - }, - undefined, - () => { - return Promise.resolve(); - }, - this.bidStatusHandler.bind(this) - ); - this.connectionSvm = new Connection(endpointSvm, "confirmed"); - } - - async bidStatusHandler(bidStatus: BidStatusUpdate) { - let resultDetails = ""; - if (bidStatus.type == "submitted" || bidStatus.type == "won") { - resultDetails = `, transaction ${bidStatus.result}`; - } else if (bidStatus.type == "lost") { - if (bidStatus.result) { - resultDetails = `, transaction ${bidStatus.result}`; - } - } - console.log( - `Bid status for bid ${bidStatus.id}: ${bidStatus.type}${resultDetails}` - ); - } - - async dummyBid() { - const secretKey = anchor.utils.bytes.bs58.decode(this.privateKey); - const searcher = Keypair.fromSecretKey(secretKey); - - const provider = new AnchorProvider( - this.connectionSvm, - new anchor.Wallet(searcher), - {} - ); - const dummy = new Program(dummyIdl as Dummy, provider); - - const permission = PublicKey.default; - const router = Keypair.generate().publicKey; - - const svmConstants = SVM_CONSTANTS[this.chainId]; - if (!(this.chainId in DUMMY_PIDS)) { - throw new Error(`Dummy program id not found for chain ${this.chainId}`); - } - const dummyPid = DUMMY_PIDS[this.chainId]; - - const configRouter = getConfigRouterPda(this.chainId, router); - const expressRelayMetadata = getExpressRelayMetadataPda(this.chainId); - const accounting = PublicKey.findProgramAddressSync( - [anchor.utils.bytes.utf8.encode("accounting")], - dummyPid - )[0]; - - const bidAmount = new anchor.BN(argv.bid); - - const ixDummy = await dummy.methods - .doNothing() - .accountsStrict({ - payer: searcher.publicKey, - expressRelay: svmConstants.expressRelayProgram, - expressRelayMetadata, - sysvarInstructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, - permission, - router, - configRouter, - accounting, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .instruction(); - ixDummy.programId = dummyPid; - - const txRaw = new anchor.web3.Transaction().add(ixDummy); - const expressRelayConfig = await this.client.getExpressRelaySvmConfig( - this.chainId, - this.connectionSvm - ); - const bid = await this.client.constructSvmBid( - txRaw, - searcher.publicKey, - router, - permission, - bidAmount, - new anchor.BN(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)), - this.chainId, - expressRelayConfig.relayerSigner, - expressRelayConfig.feeReceiverRelayer - ); - - try { - const { blockhash } = await this.connectionSvm.getLatestBlockhash(); - bid.transaction.recentBlockhash = blockhash; - bid.transaction.sign(Keypair.fromSecretKey(secretKey)); - const bidId = await this.client.submitBid(bid); - console.log(`Successful bid. Bid id ${bidId}`); - } catch (error) { - console.error(`Failed to bid: ${error}`); - } - } - - async start() { - for (;;) { - await this.dummyBid(); - await new Promise((resolve) => setTimeout(resolve, 2000)); - } - } -} - -const argv = yargs(hideBin(process.argv)) - .option("endpoint-express-relay", { - description: - "Express relay endpoint. e.g: https://per-staging.dourolabs.app/", - type: "string", - demandOption: true, - }) - .option("chain-id", { - description: "Chain id to fetch opportunities for. e.g: solana", - type: "string", - demandOption: true, - }) - .option("bid", { - description: "Bid amount in lamports", - type: "string", - default: "100", - }) - .option("private-key", { - description: "Private key to sign the bid with. In 64-byte base58 format", - type: "string", - demandOption: true, - }) - .option("api-key", { - description: - "The API key of the searcher to authenticate with the server for fetching and submitting bids", - type: "string", - demandOption: false, - }) - .option("endpoint-svm", { - description: "SVM RPC endpoint", - type: "string", - demandOption: true, - }) - .help() - .alias("help", "h") - .parseSync(); -async function run() { - if (SVM_CONSTANTS[argv.chainId] === undefined) { - throw new Error(`SVM constants not found for chain ${argv.chainId}`); - } - const searcherSvm = Keypair.fromSecretKey( - anchor.utils.bytes.bs58.decode(argv.privateKey) - ); - console.log(`Using searcher pubkey: ${searcherSvm.publicKey.toBase58()}`); - - const simpleSearcher = new SimpleSearcherSvm( - argv.endpointExpressRelay, - argv.chainId, - argv.privateKey, - argv.endpointSvm, - argv.apiKey - ); - await simpleSearcher.start(); -} - -run(); diff --git a/sdk/js/src/examples/testingSearcherLimo.ts b/sdk/js/src/examples/testingSearcherLimo.ts new file mode 100644 index 00000000..879018eb --- /dev/null +++ b/sdk/js/src/examples/testingSearcherLimo.ts @@ -0,0 +1,90 @@ +import { Opportunity } from "../index"; + +import * as anchor from "@coral-xyz/anchor"; +import { Keypair } from "@solana/web3.js"; +import { OrderStateAndAddress } from "@kamino-finance/limo-sdk/dist/utils"; +import { + getKeypair, + makeParser, + SimpleSearcherLimo, +} from "./simpleSearcherLimo"; +import { Decimal } from "decimal.js"; + +class SearcherLimo extends SimpleSearcherLimo { + private readonly fillRate: anchor.BN; + + constructor( + endpointExpressRelay: string, + chainId: string, + searcher: Keypair, + endpointSvm: string, + bid: number, + fillRate: number, + public withLatency: boolean, + public bidMargin: number, + public apiKey?: string + ) { + super(endpointExpressRelay, chainId, searcher, endpointSvm, bid, apiKey); + this.fillRate = new Decimal(fillRate).div(new Decimal(100)); + } + + async getBidAmount(): Promise { + const margin = new anchor.BN( + Math.floor(Math.random() * (this.bidMargin * 2 + 1)) - this.bidMargin + ); + return this.bid.add(margin); + } + + async opportunityHandler(opportunity: Opportunity): Promise { + if (this.withLatency) { + const latency = Math.floor(Math.random() * 500); + console.log(`Adding latency of ${latency}ms`); + await new Promise((resolve) => setTimeout(resolve, latency)); + } + return super.opportunityHandler(opportunity); + } + + protected getEffectiveFillRate(order: OrderStateAndAddress): Decimal { + return Decimal.min(this.fillRate, super.getEffectiveFillRate(order)); + } +} + +async function run() { + const argv = makeParser() + .option("fill-rate", { + description: + "How much of the initial order size to fill in percentage. Default is 100%", + type: "number", + default: 100, + }) + .option("with-latency", { + description: + "Whether to add random latency to the bid submission. Default is false", + type: "boolean", + default: false, + }) + .option("bid-margin", { + description: + "The margin to add or subtract from the bid. For example, 1 means the bid range is [bid - 1, bid + 1]. Default is 0", + type: "number", + default: 0, + }) + .parseSync(); + const searcherKeyPair = getKeypair(argv.privateKey, argv.privateKeyJsonFile); + const simpleSearcher = new SearcherLimo( + argv.endpointExpressRelay, + argv.chainId, + searcherKeyPair, + argv.endpointSvm, + argv.bid, + argv.fillRate, + argv.withLatency, + argv.bidMargin, + argv.apiKey + ); + await simpleSearcher.start(); +} + +if (require.main === module) { + run(); +} 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 8ba8712b..515d33d7 100644 --- a/sdk/python/express_relay/searcher/examples/simple_searcher_svm.py +++ b/sdk/python/express_relay/searcher/examples/simple_searcher_svm.py @@ -1,17 +1,16 @@ import argparse import asyncio import logging -import random import typing -from typing import List from decimal import Decimal +from typing import List from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Finalized +from solders.instruction import Instruction 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, @@ -29,21 +28,6 @@ from express_relay.svm.limo_client import LimoClient, OrderStateAndAddress 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: @@ -58,9 +42,6 @@ def __init__( bid_amount: int, chain_id: str, svm_rpc_endpoint: str, - fill_rate: int, - with_latency: bool, - bid_margin: int, api_key: str | None = None, ): self.client = ExpressRelayClient( @@ -79,13 +60,24 @@ def __init__( self.svm_config = SVM_CONFIGS[self.chain_id] self.rpc_client = AsyncClient(svm_rpc_endpoint) self.limo_client = LimoClient(self.rpc_client) - self.fill_rate = fill_rate - self.with_latency = with_latency - self.bid_margin = bid_margin self.express_relay_metadata = None self.mint_decimals_cache = {} self.recent_blockhash = {} + self.logger = logging.getLogger('searcher') + self.setup_logger() + self.logger.info("Using searcher pubkey: %s", self.private_key.pubkey()) + + def setup_logger(self): + self.logger.setLevel(logging.INFO) + log_handler = logging.StreamHandler() + formatter = logging.Formatter( + "%(asctime)s %(levelname)s:%(name)s:%(module)s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + log_handler.setFormatter(formatter) + self.logger.addHandler(log_handler) + async def opportunity_callback(self, opp: Opportunity): """ Callback function to run when a new opportunity is found. @@ -94,22 +86,17 @@ async def opportunity_callback(self, opp: Opportunity): 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") + self.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: bid_id = await self.client.submit_bid(bid) - logger.info( - f"Submitted bid {str(bid_id)} for opportunity {str(opp.opportunity_id)}" - ) + self.logger.info(f"Submitted bid {str(bid_id)} for opportunity {str(opp.opportunity_id)}") except Exception as e: - logger.error( + self.logger.error( f"Error submitting bid for opportunity {str(opp.opportunity_id)}: {e}" ) @@ -130,7 +117,7 @@ async def bid_status_callback(self, bid_status_update: BidStatusUpdate): elif status == BidStatusVariantsSvm.LOST: if result: result_details = f", transaction {result}" - logger.info(f"Bid status for bid {id}: {status.value}{result_details}") + self.logger.info(f"Bid status for bid {id}: {status.value}{result_details}") async def get_mint_decimals(self, mint: Pubkey) -> int: if str(mint) not in self.mint_decimals_cache: @@ -151,17 +138,20 @@ async def assess_opportunity(self, opp: OpportunitySvm) -> BidSvm | 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) + bid_amount = await self.get_bid_amount(order) + router = self.limo_client.get_pda_authority( + self.limo_client.get_program_id(), order["state"].global_config + ) submit_bid_ix = self.client.get_svm_submit_bid_instruction( searcher=self.private_key.pubkey(), - router=bid_data.router, + router=router, permission_key=order["address"], - bid_amount=bid_data.bid_amount, + bid_amount=bid_amount, deadline=DEADLINE, chain_id=self.chain_id, - fee_receiver_relayer=bid_data.relayer_fee_receiver, - relayer_signer=bid_data.relayer_signer, + fee_receiver_relayer=(await self.get_metadata()).fee_receiver_relayer, + relayer_signer=(await self.get_metadata()).relayer_signer, ) transaction = Transaction.new_with_payer( [submit_bid_ix] + ixs_take_order, self.private_key.pubkey() @@ -183,10 +173,7 @@ async def generate_take_order_ixs( Returns: A list of Limo instructions to take an order. """ - input_amount = min( - order["state"].remaining_input_amount, - order["state"].initial_input_amount * self.fill_rate // 100, - ) + input_amount = self.get_input_amount(order) output_amount = ( order["state"].expected_output_amount * input_amount + order["state"].initial_input_amount @@ -195,9 +182,10 @@ async def generate_take_order_ixs( input_mint_decimals = await self.get_mint_decimals(order["state"].input_mint) output_mint_decimals = await self.get_mint_decimals(order["state"].output_mint) - logger.info( + self.logger.info( f"Order address {order['address']}\n" - f"Sell token {order['state'].input_mint} amount: {Decimal(input_amount) / Decimal(10**input_mint_decimals)}\n" + f"Fill rate {input_amount / order['state'].initial_input_amount}\n" + f"Sell token {order['state'].input_mint} amount: {Decimal(input_amount) / Decimal(10 ** input_mint_decimals)}\n" f"Buy token {order['state'].output_mint} amount: {Decimal(output_amount) / Decimal(10**output_mint_decimals)}" ) ixs_take_order = await self.limo_client.take_order_ix( @@ -209,23 +197,10 @@ async def generate_take_order_ixs( ) 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) + def get_input_amount(self, order: OrderStateAndAddress) -> int: + return order["state"].remaining_input_amount + async def get_metadata(self) -> ExpressRelayMetadata: if self.express_relay_metadata is None: self.express_relay_metadata = await ExpressRelayMetadata.fetch( self.rpc_client, @@ -236,13 +211,17 @@ async def get_bid_data(self, order: OrderStateAndAddress) -> BidData: ) if self.express_relay_metadata is None: raise ValueError("Express relay metadata account not found") + return self.express_relay_metadata - return BidData( - router=router, - bid_amount=bid_amount, - relayer_signer=self.express_relay_metadata.relayer_signer, - relayer_fee_receiver=self.express_relay_metadata.fee_receiver_relayer, - ) + async def get_bid_amount(self, order: OrderStateAndAddress) -> int: + """ + Args: + order: An object representing the order to be fulfilled. + Returns: + The amount of bid to submit for the opportunity in lamports. + """ + + return self.bid_amount async def svm_chain_update_callback(self, svm_chain_update: SvmChainUpdate): self.recent_blockhash[svm_chain_update.chain_id] = svm_chain_update.blockhash @@ -254,9 +233,8 @@ async def remove_opportunities_callback( print(f"Opportunities {opportunity_delete} don't exist anymore") -async def main(): +def get_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser() - parser.add_argument("-v", "--verbose", action="count", default=0) group = parser.add_mutually_exclusive_group(required=True) group.add_argument( "--private-key", @@ -272,6 +250,7 @@ async def main(): "--chain-id", type=str, required=True, + choices=SVM_CONFIGS.keys(), help="Chain ID of the SVM network to submit bids", ) parser.add_argument( @@ -298,37 +277,12 @@ async def main(): default=100, help="The amount of bid to submit for each opportunity", ) - parser.add_argument( - "--fill-rate", - type=int, - default=100, - help="How much of the initial order size to fill in percentage. Default is 100%", - ) - parser.add_argument( - "--with-latency", - required=False, - default=False, - action="store_true", - help="Whether to add random latency to the bid submission. Default is false", - ) - parser.add_argument( - "--bid-margin", - required=False, - type=int, - default=0, - help="The margin to add or subtract from the bid. For example, 1 means the bid range is [bid - 1, bid + 1]. Default is 0", - ) + return parser - args = parser.parse_args() - logger.setLevel(logging.INFO if args.verbose == 0 else logging.DEBUG) - log_handler = logging.StreamHandler() - formatter = logging.Formatter( - "%(asctime)s %(levelname)s:%(name)s:%(module)s %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - log_handler.setFormatter(formatter) - logger.addHandler(log_handler) +async def main(): + parser = get_parser() + args = parser.parse_args() if args.private_key: searcher_keypair = Keypair.from_base58_string(args.private_key) @@ -336,16 +290,12 @@ async def main(): with open(args.private_key_json_file, "r") as f: searcher_keypair = Keypair.from_json(f.read()) - logger.info("Using Keypair with pubkey: %s", searcher_keypair.pubkey()) searcher = SimpleSearcherSvm( args.endpoint_express_relay, searcher_keypair, args.bid, args.chain_id, args.endpoint_svm, - args.fill_rate, - args.with_latency, - args.bid_margin, args.api_key, ) diff --git a/sdk/python/express_relay/searcher/examples/testing_searcher_svm.py b/sdk/python/express_relay/searcher/examples/testing_searcher_svm.py new file mode 100644 index 00000000..d6b5699e --- /dev/null +++ b/sdk/python/express_relay/searcher/examples/testing_searcher_svm.py @@ -0,0 +1,86 @@ +import asyncio +import random +from decimal import Decimal + +from solders.keypair import Keypair + +from express_relay.models import ( + Opportunity +) +from express_relay.svm.limo_client import OrderStateAndAddress +from .simple_searcher_svm import get_parser, SimpleSearcherSvm + + +class TestingSearcherSvm(SimpleSearcherSvm): + + def __init__(self, server_url: str, private_key: Keypair, bid_amount: int, chain_id: str, svm_rpc_endpoint: str, + fill_rate: int, with_latency: bool, bid_margin: int, api_key: str | None = None): + super().__init__(server_url, private_key, bid_amount, chain_id, svm_rpc_endpoint, api_key) + self.fill_rate = fill_rate + self.with_latency = with_latency + self.bid_margin = bid_margin + + async def opportunity_callback(self, opp: Opportunity): + if self.with_latency: + latency = 0.5 * random.random() + self.logger.info(f"Adding latency of {latency * 100}ms") + await asyncio.sleep(latency) + return await super().opportunity_callback(opp) + + async def get_bid_amount(self, order: OrderStateAndAddress) -> int: + return self.bid_amount + random.randint(-self.bid_margin, self.bid_margin) + + def get_input_amount(self, order: OrderStateAndAddress) -> int: + return min(super().get_input_amount(order), order["state"].initial_input_amount * self.fill_rate // 100) + + +async def main(): + parser = get_parser() + parser.add_argument( + "--fill-rate", + type=int, + default=100, + help="How much of the initial order size to fill in percentage. Default is 100%", + ) + parser.add_argument( + "--with-latency", + required=False, + default=False, + action="store_true", + help="Whether to add random latency to the bid submission. Default is false", + ) + parser.add_argument( + "--bid-margin", + required=False, + type=int, + default=0, + help="The margin to add or subtract from the bid. For example, 1 means the bid range is [bid - 1, bid + 1]. Default is 0", + ) + args = parser.parse_args() + + if args.private_key: + searcher_keypair = Keypair.from_base58_string(args.private_key) + else: + with open(args.private_key_json_file, "r") as private_key_file: + searcher_keypair = Keypair.from_json(private_key_file.read()) + + searcher = TestingSearcherSvm( + args.endpoint_express_relay, + searcher_keypair, + args.bid, + args.chain_id, + args.endpoint_svm, + args.fill_rate, + args.with_latency, + args.bid_margin, + args.api_key, + ) + + await searcher.client.subscribe_chains([args.chain_id]) + + task = await searcher.client.get_ws_loop() + await task + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 3b431c3a..334a21fc 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "express-relay" -version = "0.13.3" +version = "0.13.4" description = "Utilities for searchers and protocols to interact with the Express Relay protocol." authors = ["dourolabs"] license = "Apache-2.0"