Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion express_relay/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.10.0",
"version": "0.11.0",
"description": "Utilities for interacting with the express relay protocol",
"homepage": "https://github.com/pyth-network/pyth-crosschain/tree/main/express_relay/sdk/js",
"author": "Douro Labs",
Expand Down
6 changes: 0 additions & 6 deletions express_relay/sdk/js/src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@ export const OPPORTUNITY_ADAPTER_CONFIGS: Record<

export const SVM_CONSTANTS: Record<string, SvmConstantsConfig> = {
"development-solana": {
relayerSigner: new PublicKey(
"GEeEguHhepHtPVo3E9RA1wvnxgxJ61iSc9dJfd433w3K"
),
feeReceiverRelayer: new PublicKey(
"feesJcX9zwLiEZs9iQGXeBd65b9m2Zc1LjjyHngQF29"
),
expressRelayProgram: new PublicKey(
"PytERJFhAKuNNuaiXkApLfWzwNwSNDACpigT3LwQfou"
),
Expand Down
2 changes: 2 additions & 0 deletions express_relay/sdk/js/src/examples/simpleSearcherEvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class SimpleSearcherEvm {
}

async opportunityHandler(opportunity: Opportunity) {
if (!("targetContract" in opportunity))
Copy link
Contributor

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?

Copy link
Contributor Author

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.

throw new Error("Not a valid EVM opportunity");
const bidAmount = BigInt(argv.bid);
// Bid info should be generated by evaluating the opportunity
// here for simplicity we are using a constant bid and 24 hours of validity
Expand Down
131 changes: 86 additions & 45 deletions express_relay/sdk/js/src/examples/simpleSearcherLimo.ts
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";

Expand All @@ -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(
Expand All @@ -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) {
Expand All @@ -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
);
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
this.expressRelayConfig!.feeReceiverRelayer
);

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

async opportunityHandler(opportunity: Opportunity) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it okay to throw when getting invalid opp?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have subscribed to a SVM chain and receive EVM opportunities, I think you should throw and it's fine here

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]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we pass a chain_type to server with chain_id, we can remove bunch of type checkings above :-?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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();
}
}
}
Expand Down Expand Up @@ -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:
Expand All @@ -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();
Expand Down
9 changes: 7 additions & 2 deletions express_relay/sdk/js/src/examples/simpleSearcherSvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,20 @@ class SimpleSearcherSvm {
ixDummy.programId = dummyPid;

const txRaw = new anchor.web3.Transaction().add(ixDummy);

const expressRelayConfig = await this.client.getExpressRelaySvmConfig(
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 {
Expand Down
Loading
Loading