-
Couldn't load subscription status.
- Fork 302
feat: Update sdks to use svm opportunities #2009
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
f6c9129
51f65ef
4541f94
a6cac64
170d995
baf02f3
0850c9d
811dbc6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,11 @@ | ||
| import yargs from "yargs"; | ||
| import { hideBin } from "yargs/helpers"; | ||
| import { Client } from "../index"; | ||
| import { | ||
| Client, | ||
| ExpressRelaySvmConfig, | ||
| Opportunity, | ||
| OpportunitySvm, | ||
| } from "../index"; | ||
| import { BidStatusUpdate } from "../types"; | ||
| import { SVM_CONSTANTS } from "../const"; | ||
|
|
||
|
|
@@ -9,24 +14,22 @@ import { Keypair, PublicKey, Connection } from "@solana/web3.js"; | |
|
|
||
| import * as limo from "@kamino-finance/limo-sdk"; | ||
| import { Decimal } from "decimal.js"; | ||
| import { | ||
| getPdaAuthority, | ||
| OrderStateAndAddress, | ||
| } from "@kamino-finance/limo-sdk/dist/utils"; | ||
| import { getPdaAuthority } from "@kamino-finance/limo-sdk/dist/utils"; | ||
|
|
||
| const DAY_IN_SECONDS = 60 * 60 * 24; | ||
|
|
||
| class SimpleSearcherLimo { | ||
| private client: Client; | ||
| private connectionSvm: Connection; | ||
| private clientLimo: limo.LimoClient; | ||
| private searcher: Keypair; | ||
| private expressRelayConfig: ExpressRelaySvmConfig | undefined; | ||
| constructor( | ||
| public endpointExpressRelay: string, | ||
| public chainId: string, | ||
| privateKey: string, | ||
| private searcher: Keypair, | ||
| public endpointSvm: string, | ||
| public globalConfig: PublicKey, | ||
| public fillRate: number, | ||
| public apiKey?: string | ||
| ) { | ||
| this.client = new Client( | ||
|
|
@@ -35,15 +38,11 @@ class SimpleSearcherLimo { | |
| apiKey, | ||
| }, | ||
| undefined, | ||
| () => { | ||
| return Promise.resolve(); | ||
| }, | ||
| this.opportunityHandler.bind(this), | ||
| this.bidStatusHandler.bind(this) | ||
| ); | ||
| this.connectionSvm = new Connection(endpointSvm, "confirmed"); | ||
| this.clientLimo = new limo.LimoClient(this.connectionSvm, globalConfig); | ||
| const secretKey = anchor.utils.bytes.bs58.decode(privateKey); | ||
| this.searcher = Keypair.fromSecretKey(secretKey); | ||
| } | ||
|
|
||
| async bidStatusHandler(bidStatus: BidStatusUpdate) { | ||
|
|
@@ -60,7 +59,8 @@ class SimpleSearcherLimo { | |
| ); | ||
| } | ||
|
|
||
| async evaluateOrder(order: OrderStateAndAddress) { | ||
| async generateBid(opportunity: OpportunitySvm) { | ||
| const order = opportunity.order; | ||
| const inputMintDecimals = await this.clientLimo.getOrderInputMintDecimals( | ||
| order | ||
| ); | ||
|
|
@@ -69,13 +69,20 @@ class SimpleSearcherLimo { | |
| ); | ||
| const inputAmountDecimals = new Decimal( | ||
| order.state.remainingInputAmount.toNumber() | ||
| ).div(new Decimal(10).pow(inputMintDecimals)); | ||
| ) | ||
| .div(new Decimal(10).pow(inputMintDecimals)) | ||
| .mul(this.fillRate) | ||
| .div(100); | ||
|
|
||
| const outputAmountDecimals = new Decimal( | ||
| order.state.expectedOutputAmount.toNumber() | ||
| ).div(new Decimal(10).pow(outputMintDecimals)); | ||
| ) | ||
| .div(new Decimal(10).pow(outputMintDecimals)) | ||
| .mul(this.fillRate) | ||
| .div(100); | ||
|
|
||
| console.log("Order address", order.address.toBase58()); | ||
| console.log("Fill rate", this.fillRate); | ||
| console.log( | ||
| "Sell token", | ||
| order.state.inputMint.toBase58(), | ||
|
|
@@ -112,41 +119,49 @@ class SimpleSearcherLimo { | |
| order.address, | ||
| bidAmount, | ||
| new anchor.BN(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)), | ||
| this.chainId | ||
| this.chainId, | ||
| this.expressRelayConfig!.relayerSigner, | ||
danimhr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| this.expressRelayConfig!.feeReceiverRelayer | ||
| ); | ||
|
|
||
| bid.transaction.recentBlockhash = opportunity.blockHash; | ||
| bid.transaction.sign(this.searcher); | ||
| return bid; | ||
| } | ||
|
|
||
| async opportunityHandler(opportunity: Opportunity) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we change the code so that opportunityHandler get a svm opportunity as input? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so, this would affect all the subscription interfaces. |
||
| if (!("order" in opportunity)) | ||
| throw new Error("Not a valid SVM opportunity"); | ||
|
||
| const bid = await this.generateBid(opportunity); | ||
| try { | ||
| const { blockhash } = await this.connectionSvm.getLatestBlockhash(); | ||
| bid.transaction.recentBlockhash = blockhash; | ||
| bid.transaction.sign(this.searcher); | ||
| const bidId = await this.client.submitBid(bid); | ||
| console.log(`Successful bid. Bid id ${bidId}`); | ||
| console.log( | ||
| `Successful bid. Opportunity id ${opportunity.opportunityId} Bid id ${bidId}` | ||
| ); | ||
| } catch (error) { | ||
| console.error(`Failed to bid: ${error}`); | ||
| console.error( | ||
| `Failed to bid on opportunity ${opportunity.opportunityId}: ${error}` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| async bidOnNewOrders() { | ||
| let allOrders = | ||
| await this.clientLimo.getAllOrdersStateAndAddressWithFilters([]); | ||
| allOrders = allOrders.filter( | ||
| (order) => !order.state.remainingInputAmount.isZero() | ||
| async fetchConfig() { | ||
| this.expressRelayConfig = await this.client.getExpressRelaySvmConfig( | ||
| this.chainId, | ||
| this.connectionSvm | ||
| ); | ||
| if (allOrders.length === 0) { | ||
| console.log("No orders to bid on"); | ||
| return; | ||
| } | ||
| for (const order of allOrders) { | ||
| await this.evaluateOrder(order); | ||
| } | ||
| // Note: You need to parallelize this in production with something like: | ||
| // await Promise.all(allOrders.map((order) => this.evaluateOrder(order))); | ||
| } | ||
|
|
||
| async start() { | ||
| for (;;) { | ||
| await this.bidOnNewOrders(); | ||
| await new Promise((resolve) => setTimeout(resolve, 2000)); | ||
| await this.fetchConfig(); | ||
| try { | ||
| await this.client.subscribeChains([argv.chainId]); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if we pass a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. true, assuming we want to keep stuff backward compatible and not change a lot on the server side, I thought this is a good enough solution |
||
| console.log( | ||
| `Subscribed to chain ${argv.chainId}. Waiting for opportunities...` | ||
| ); | ||
| } catch (error) { | ||
| console.error(error); | ||
| this.client.websocket?.close(); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -174,9 +189,15 @@ const argv = yargs(hideBin(process.argv)) | |
| default: "100", | ||
| }) | ||
| .option("private-key", { | ||
| description: "Private key to sign the bid with. In 64-byte base58 format", | ||
| description: "Private key of the searcher in base58 format", | ||
| type: "string", | ||
| demandOption: true, | ||
| 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: | ||
|
|
@@ -189,24 +210,44 @@ const argv = yargs(hideBin(process.argv)) | |
| type: "string", | ||
| demandOption: true, | ||
| }) | ||
| .option("fill-rate", { | ||
| description: "How much of the order to fill in percentage. Default is 100%", | ||
| type: "number", | ||
| default: 100, | ||
| }) | ||
| .help() | ||
| .alias("help", "h") | ||
| .parseSync(); | ||
| async function run() { | ||
| if (!SVM_CONSTANTS[argv.chainId]) { | ||
| 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()}`); | ||
| let searcherKeyPair; | ||
|
|
||
| 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)) | ||
| ) | ||
| ); | ||
| } else { | ||
| throw new Error( | ||
| "Either private-key or private-key-json-file must be provided" | ||
| ); | ||
| } | ||
| console.log(`Using searcher pubkey: ${searcherKeyPair.publicKey.toBase58()}`); | ||
|
|
||
| const simpleSearcher = new SimpleSearcherLimo( | ||
| argv.endpointExpressRelay, | ||
| argv.chainId, | ||
| argv.privateKey, | ||
| searcherKeyPair, | ||
| argv.endpointSvm, | ||
| new PublicKey(argv.globalConfig), | ||
| argv.fillRate, | ||
| argv.apiKey | ||
| ); | ||
| await simpleSearcher.start(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,15 +102,20 @@ class SimpleSearcherSvm { | |
| ixDummy.programId = dummyPid; | ||
|
|
||
| const txRaw = new anchor.web3.Transaction().add(ixDummy); | ||
|
|
||
| const expressRelayConfig = await this.client.getExpressRelaySvmConfig( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it's a better idea to not fetch config here. We can store it on client as a singleton? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not certain how that would look like, we can discuss this. |
||
| 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 | ||
| this.chainId, | ||
| expressRelayConfig.relayerSigner, | ||
| expressRelayConfig.feeReceiverRelayer | ||
| ); | ||
|
|
||
| try { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we trying to validate fields and type here or just checking if it's a Svm or Evm opp?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just checking it's an Svm/Evm opp.