@pythnetwork/express-relay-evm-js
Advanced tools
Comparing version 0.1.0 to 0.1.1
@@ -11,5 +11,2 @@ "use strict"; | ||
const viem_1 = require("viem"); | ||
function sleep(ms) { | ||
return new Promise((resolve) => setTimeout(resolve, ms)); | ||
} | ||
const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) | ||
@@ -24,2 +21,3 @@ .option("endpoint", { | ||
type: "string", | ||
demandOption: true, | ||
}) | ||
@@ -49,26 +47,28 @@ .option("bid", { | ||
const DAY_IN_SECONDS = 60 * 60 * 24; | ||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
const opportunities = await client.getOpportunities(argv.chainId); | ||
console.log(`Fetched ${opportunities.length} opportunities`); | ||
for (const opportunity of opportunities) { | ||
const bid = 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 | ||
const bidInfo = { | ||
amount: bid, | ||
validUntil: BigInt(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)), | ||
}; | ||
const opportunityBid = await client.signOpportunityBid(opportunity, bidInfo, argv.privateKey); | ||
try { | ||
await client.submitOpportunityBid(opportunityBid); | ||
console.log(`Successful bid ${bid} on opportunity ${opportunity.opportunityId}`); | ||
} | ||
catch (error) { | ||
console.error(`Failed to bid on opportunity ${opportunity.opportunityId}: ${error}`); | ||
} | ||
client.setOpportunityHandler(async (opportunity) => { | ||
const bid = 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 | ||
const bidInfo = { | ||
amount: bid, | ||
validUntil: BigInt(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)), | ||
}; | ||
const opportunityBid = await client.signOpportunityBid(opportunity, bidInfo, (0, index_1.checkHex)(argv.privateKey)); | ||
try { | ||
await client.submitOpportunityBid(opportunityBid); | ||
console.log(`Successful bid ${bid} on opportunity ${opportunity.opportunityId}`); | ||
} | ||
await sleep(5000); | ||
catch (error) { | ||
console.error(`Failed to bid on opportunity ${opportunity.opportunityId}: ${error}`); | ||
} | ||
}); | ||
try { | ||
await client.subscribeChains([argv.chainId]); | ||
console.log(`Subscribed to chain ${argv.chainId}. Waiting for opportunities...`); | ||
} | ||
catch (error) { | ||
console.error(error); | ||
client.websocket?.close(); | ||
} | ||
} | ||
run(); |
@@ -1,3 +0,6 @@ | ||
import { ClientOptions } from "openapi-fetch"; | ||
/// <reference types="ws" /> | ||
import type { components } from "./types"; | ||
import { ClientOptions as FetchClientOptions } from "openapi-fetch"; | ||
import { Address, Hex } from "viem"; | ||
import WebSocket from "isomorphic-ws"; | ||
/** | ||
@@ -84,6 +87,44 @@ * ERC20 token with contract address and amount | ||
export declare function checkAddress(address: string): Address; | ||
type ClientOptions = FetchClientOptions & { | ||
baseUrl: string; | ||
}; | ||
export interface WsOptions { | ||
/** | ||
* Max time to wait for a response from the server in milliseconds | ||
*/ | ||
response_timeout: number; | ||
} | ||
export declare class Client { | ||
private clientOptions?; | ||
constructor(clientOptions?: ClientOptions); | ||
clientOptions: ClientOptions; | ||
wsOptions: WsOptions; | ||
websocket?: WebSocket; | ||
idCounter: number; | ||
callbackRouter: Record<string, (response: components["schemas"]["ServerResultMessage"]) => void>; | ||
private websocketOpportunityCallback?; | ||
constructor(clientOptions: ClientOptions, wsOptions?: WsOptions); | ||
private connectWebsocket; | ||
/** | ||
* Converts an opportunity from the server to the client format | ||
* Returns undefined if the opportunity version is not supported | ||
* @param opportunity | ||
*/ | ||
private convertOpportunity; | ||
setOpportunityHandler(callback: (opportunity: Opportunity) => Promise<void>): void; | ||
/** | ||
* Subscribes to the specified chains | ||
* | ||
* The opportunity handler will be called for opportunities on the specified chains | ||
* If the opportunity handler is not set, an error will be thrown | ||
* @param chains | ||
*/ | ||
subscribeChains(chains: string[]): Promise<void>; | ||
/** | ||
* Unsubscribes from the specified chains | ||
* | ||
* The opportunity handler will no longer be called for opportunities on the specified chains | ||
* @param chains | ||
*/ | ||
unsubscribeChains(chains: string[]): Promise<void>; | ||
sendWebsocketMessage(msg: components["schemas"]["ClientMessage"]): Promise<void>; | ||
/** | ||
* Fetches liquidation opportunities | ||
@@ -111,2 +152,3 @@ * @param chainId Chain id to fetch opportunities for. e.g: sepolia | ||
} | ||
export {}; | ||
//# sourceMappingURL=index.d.ts.map |
152
lib/index.js
@@ -10,2 +10,3 @@ "use strict"; | ||
const accounts_1 = require("viem/accounts"); | ||
const isomorphic_ws_1 = __importDefault(require("isomorphic-ws")); | ||
function checkHex(hex) { | ||
@@ -31,8 +32,138 @@ if ((0, viem_1.isHex)(hex)) { | ||
} | ||
const DEFAULT_WS_OPTIONS = { | ||
response_timeout: 5000, | ||
}; | ||
class Client { | ||
clientOptions; | ||
constructor(clientOptions) { | ||
wsOptions; | ||
websocket; | ||
idCounter = 0; | ||
callbackRouter = {}; | ||
websocketOpportunityCallback; | ||
constructor(clientOptions, wsOptions) { | ||
this.clientOptions = clientOptions; | ||
this.wsOptions = { ...DEFAULT_WS_OPTIONS, ...wsOptions }; | ||
} | ||
connectWebsocket() { | ||
const websocketEndpoint = new URL(this.clientOptions.baseUrl); | ||
websocketEndpoint.protocol = | ||
websocketEndpoint.protocol === "https:" ? "wss:" : "ws:"; | ||
websocketEndpoint.pathname = "/v1/ws"; | ||
this.websocket = new isomorphic_ws_1.default(websocketEndpoint.toString()); | ||
this.websocket.on("message", async (data) => { | ||
const message = JSON.parse(data.toString()); | ||
if ("id" in message && message.id) { | ||
const callback = this.callbackRouter[message.id]; | ||
if (callback !== undefined) { | ||
callback(message); | ||
delete this.callbackRouter[message.id]; | ||
} | ||
} | ||
else if ("type" in message && message.type === "new_opportunity") { | ||
if (this.websocketOpportunityCallback !== undefined) { | ||
const convertedOpportunity = this.convertOpportunity(message.opportunity); | ||
if (convertedOpportunity !== undefined) { | ||
await this.websocketOpportunityCallback(convertedOpportunity); | ||
} | ||
} | ||
} | ||
else if ("error" in message) { | ||
// Can not route error messages to the callback router as they don't have an id | ||
console.error(message.error); | ||
} | ||
}); | ||
} | ||
/** | ||
* Converts an opportunity from the server to the client format | ||
* Returns undefined if the opportunity version is not supported | ||
* @param opportunity | ||
*/ | ||
convertOpportunity(opportunity) { | ||
if (opportunity.version != "v1") { | ||
console.warn(`Can not handle opportunity version: ${opportunity.version}. Please upgrade your client.`); | ||
return undefined; | ||
} | ||
return { | ||
chainId: opportunity.chain_id, | ||
opportunityId: opportunity.opportunity_id, | ||
permissionKey: checkHex(opportunity.permission_key), | ||
contract: checkAddress(opportunity.contract), | ||
calldata: checkHex(opportunity.calldata), | ||
value: BigInt(opportunity.value), | ||
repayTokens: opportunity.repay_tokens.map(checkTokenQty), | ||
receiptTokens: opportunity.receipt_tokens.map(checkTokenQty), | ||
}; | ||
} | ||
setOpportunityHandler(callback) { | ||
this.websocketOpportunityCallback = callback; | ||
} | ||
/** | ||
* Subscribes to the specified chains | ||
* | ||
* The opportunity handler will be called for opportunities on the specified chains | ||
* If the opportunity handler is not set, an error will be thrown | ||
* @param chains | ||
*/ | ||
async subscribeChains(chains) { | ||
if (this.websocketOpportunityCallback === undefined) { | ||
throw new Error("Opportunity handler not set"); | ||
} | ||
return this.sendWebsocketMessage({ | ||
method: "subscribe", | ||
params: { | ||
chain_ids: chains, | ||
}, | ||
}); | ||
} | ||
/** | ||
* Unsubscribes from the specified chains | ||
* | ||
* The opportunity handler will no longer be called for opportunities on the specified chains | ||
* @param chains | ||
*/ | ||
async unsubscribeChains(chains) { | ||
return this.sendWebsocketMessage({ | ||
method: "unsubscribe", | ||
params: { | ||
chain_ids: chains, | ||
}, | ||
}); | ||
} | ||
async sendWebsocketMessage(msg) { | ||
const msg_with_id = { | ||
...msg, | ||
id: (this.idCounter++).toString(), | ||
}; | ||
return new Promise((resolve, reject) => { | ||
this.callbackRouter[msg_with_id.id] = (response) => { | ||
if (response.status === "success") { | ||
resolve(); | ||
} | ||
else { | ||
reject(response.result); | ||
} | ||
}; | ||
if (this.websocket === undefined) { | ||
this.connectWebsocket(); | ||
} | ||
if (this.websocket !== undefined) { | ||
if (this.websocket.readyState === isomorphic_ws_1.default.CONNECTING) { | ||
this.websocket.on("open", () => { | ||
this.websocket?.send(JSON.stringify(msg_with_id)); | ||
}); | ||
} | ||
else if (this.websocket.readyState === isomorphic_ws_1.default.OPEN) { | ||
this.websocket.send(JSON.stringify(msg_with_id)); | ||
} | ||
else { | ||
reject("Websocket connection closing or already closed"); | ||
} | ||
} | ||
setTimeout(() => { | ||
delete this.callbackRouter[msg_with_id.id]; | ||
reject("Websocket response timeout"); | ||
}, this.wsOptions.response_timeout); | ||
}); | ||
} | ||
/** | ||
* Fetches liquidation opportunities | ||
@@ -50,16 +181,7 @@ * @param chainId Chain id to fetch opportunities for. e.g: sepolia | ||
return opportunities.data.flatMap((opportunity) => { | ||
if (opportunity.version != "v1") { | ||
console.warn(`Can not handle opportunity version: ${opportunity.version}. Please upgrade your client.`); | ||
const convertedOpportunity = this.convertOpportunity(opportunity); | ||
if (convertedOpportunity === undefined) { | ||
return []; | ||
} | ||
return { | ||
chainId: opportunity.chain_id, | ||
opportunityId: opportunity.opportunity_id, | ||
permissionKey: checkHex(opportunity.permission_key), | ||
contract: checkAddress(opportunity.contract), | ||
calldata: checkHex(opportunity.calldata), | ||
value: BigInt(opportunity.value), | ||
repayTokens: opportunity.repay_tokens.map(checkTokenQty), | ||
receiptTokens: opportunity.receipt_tokens.map(checkTokenQty), | ||
}; | ||
return convertedOpportunity; | ||
}); | ||
@@ -136,2 +258,3 @@ } | ||
{ name: "bid", type: "uint256" }, | ||
{ name: "validUntil", type: "uint256" }, | ||
], [ | ||
@@ -144,4 +267,5 @@ opportunity.repayTokens.map(convertTokenQty), | ||
bidInfo.amount, | ||
bidInfo.validUntil, | ||
]); | ||
const msgHash = (0, viem_1.keccak256)((0, viem_1.encodePacked)(["bytes", "uint256"], [payload, bidInfo.validUntil])); | ||
const msgHash = (0, viem_1.keccak256)(payload); | ||
const hash = (0, accounts_1.signatureToHex)(await (0, accounts_1.sign)({ hash: msgHash, privateKey })); | ||
@@ -148,0 +272,0 @@ return { |
{ | ||
"name": "@pythnetwork/express-relay-evm-js", | ||
"version": "0.1.0", | ||
"version": "0.1.1", | ||
"description": "Utilities for interacting with the express relay protocol", | ||
@@ -39,5 +39,7 @@ "homepage": "https://pyth.network", | ||
"dependencies": { | ||
"isomorphic-ws": "^5.0.0", | ||
"openapi-client-axios": "^7.5.4", | ||
"openapi-fetch": "^0.8.2", | ||
"viem": "^2.7.6" | ||
"viem": "^2.7.6", | ||
"ws": "^8.16.0" | ||
}, | ||
@@ -56,3 +58,3 @@ "devDependencies": { | ||
"license": "Apache-2.0", | ||
"gitHead": "6771c2c6998f53effee9247347cb0ac71612b3dc" | ||
"gitHead": "0d49986eb1cd77c969059bcb72857e90ba8ae4f8" | ||
} |
@@ -38,13 +38,10 @@ # Pyth Express Relay JS SDK | ||
function calculateOpportunityBid( | ||
opportunity: OpportunityParams | ||
): BidInfo | null { | ||
function calculateOpportunityBid(opportunity: Opportunity): BidInfo | null { | ||
// searcher implementation here | ||
// if the opportunity is not suitable for the searcher, return null | ||
} | ||
const opportunities = await client.getOpportunities(); | ||
for (const opportunity of opportunities) { | ||
const bidInfo = calculateOpportunityBid(order); | ||
if (bidInfo === null) continue; | ||
client.setOpportunityHandler(async (opportunity: Opportunity) => { | ||
const bidInfo = calculateOpportunityBid(opportunity); | ||
if (bidInfo === null) return; | ||
const opportunityBid = await client.signOpportunityBid( | ||
@@ -56,3 +53,4 @@ opportunity, | ||
await client.submitOpportunityBid(opportunityBid); | ||
} | ||
}); | ||
await client.subscribeChains([chain_id]); // chain id you want to subscribe to | ||
``` | ||
@@ -59,0 +57,0 @@ |
Sorry, the diff of this file is not supported yet
24611
519
5
73
+ Addedisomorphic-ws@^5.0.0
+ Addedws@^8.16.0
+ Addedisomorphic-ws@5.0.0(transitive)