From 3062c256cfa854940f117ffa6d08fe53e8ad3975 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Thu, 29 Feb 2024 13:31:28 +0100 Subject: [PATCH 1/9] feat(per-js-sdk): Add support for bid state and raw bid submission --- express_relay/sdk/js/package.json | 2 +- .../sdk/js/src/examples/SimpleSearcher.ts | 11 +- express_relay/sdk/js/src/index.ts | 167 +++++++++++++++--- express_relay/sdk/js/src/types.d.ts | 138 +++++++++++++-- package-lock.json | 2 +- 5 files changed, 275 insertions(+), 45 deletions(-) diff --git a/express_relay/sdk/js/package.json b/express_relay/sdk/js/package.json index 125c07f20d..a64fa0f4c4 100644 --- a/express_relay/sdk/js/package.json +++ b/express_relay/sdk/js/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/express-relay-evm-js", - "version": "0.1.1", + "version": "0.1.2", "description": "Utilities for interacting with the express relay protocol", "homepage": "https://pyth.network", "author": { diff --git a/express_relay/sdk/js/src/examples/SimpleSearcher.ts b/express_relay/sdk/js/src/examples/SimpleSearcher.ts index 6329e0e23b..254d2d948e 100644 --- a/express_relay/sdk/js/src/examples/SimpleSearcher.ts +++ b/express_relay/sdk/js/src/examples/SimpleSearcher.ts @@ -40,6 +40,11 @@ async function run() { throw new Error(`Invalid private key: ${argv.privateKey}`); } const DAY_IN_SECONDS = 60 * 60 * 24; + client.setBidStatusHandler(async (bidStatus) => { + console.log( + `Bid status for bid ${bidStatus.id}: ${bidStatus.status.status}` + ); + }); client.setOpportunityHandler(async (opportunity) => { const bid = BigInt(argv.bid); // Bid info should be generated by evaluating the opportunity @@ -54,9 +59,11 @@ async function run() { checkHex(argv.privateKey) ); try { - await client.submitOpportunityBid(opportunityBid); + const bidId = await client.submitOpportunityBidViaWebsocket( + opportunityBid + ); console.log( - `Successful bid ${bid} on opportunity ${opportunity.opportunityId}` + `Successful bid. Opportunity id ${opportunityBid.opportunityId} Bid id ${bidId}` ); } catch (error) { console.error( diff --git a/express_relay/sdk/js/src/index.ts b/express_relay/sdk/js/src/index.ts index 8e9eb387af..0a87204fe2 100644 --- a/express_relay/sdk/js/src/index.ts +++ b/express_relay/sdk/js/src/index.ts @@ -5,7 +5,6 @@ import createClient, { import { Address, encodeAbiParameters, - encodePacked, Hex, isAddress, isHex, @@ -13,6 +12,7 @@ import { } from "viem"; import { privateKeyToAccount, sign, signatureToHex } from "viem/accounts"; import WebSocket from "isomorphic-ws"; + /** * ERC20 token with contract address and amount */ @@ -21,6 +21,9 @@ export type TokenQty = { amount: bigint; }; +export type BidId = string; +export type ChainId = string; + /** * Bid information */ @@ -42,7 +45,7 @@ export type Opportunity = { /** * The chain id where the liquidation will be executed. */ - chainId: string; + chainId: ChainId; /** * Unique identifier for the opportunity @@ -99,6 +102,43 @@ export type OpportunityBid = { bid: BidInfo; }; +/** + * Represents a raw bid on acquiring a permission key + */ +export type Bid = { + /** + * The permission key to bid on + * @example 0xc0ffeebabe + * + */ + permissionKey: Hex; + /** + * @description Amount of bid in wei. + * @example 10 + */ + amount: bigint; + /** + * @description Calldata for the contract call. + * @example 0xdeadbeef + */ + calldata: Hex; + /** + * @description The chain id to bid on. + * @example sepolia + */ + chainId: ChainId; + /** + * @description The contract address to call. + * @example 0xcA11bde05977b3631167028862bE2a173976CA11 + */ + contract: Address; +}; + +export type BidStatusUpdate = { + id: BidId; + status: components["schemas"]["BidStatus"]; +}; + export function checkHex(hex: string): Hex { if (isHex(hex)) { return hex; @@ -146,6 +186,10 @@ export class Client { opportunity: Opportunity ) => Promise; + private websocketBidStatusCallback?: ( + statusUpdate: BidStatusUpdate + ) => Promise; + constructor(clientOptions: ClientOptions, wsOptions?: WsOptions) { this.clientOptions = clientOptions; this.wsOptions = { ...DEFAULT_WS_OPTIONS, ...wsOptions }; @@ -164,13 +208,7 @@ export class Client { | components["schemas"]["ServerUpdateResponse"] = 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 ("type" in message && message.type === "new_opportunity") { if (this.websocketOpportunityCallback !== undefined) { const convertedOpportunity = this.convertOpportunity( message.opportunity @@ -179,6 +217,16 @@ export class Client { await this.websocketOpportunityCallback(convertedOpportunity); } } + } else if ("type" in message && message.type === "bid_status_update") { + if (this.websocketBidStatusCallback !== undefined) { + await this.websocketBidStatusCallback(message); + } + } else if ("id" in message && message.id) { + const callback = this.callbackRouter[message.id]; + if (callback !== undefined) { + callback(message); + delete this.callbackRouter[message.id]; + } } 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); @@ -218,6 +266,12 @@ export class Client { this.websocketOpportunityCallback = callback; } + public setBidStatusHandler( + callback: (statusUpdate: BidStatusUpdate) => Promise + ) { + this.websocketBidStatusCallback = callback; + } + /** * Subscribes to the specified chains * @@ -225,11 +279,11 @@ export class Client { * If the opportunity handler is not set, an error will be thrown * @param chains */ - async subscribeChains(chains: string[]) { + async subscribeChains(chains: string[]): Promise { if (this.websocketOpportunityCallback === undefined) { throw new Error("Opportunity handler not set"); } - return this.sendWebsocketMessage({ + await this.sendWebsocketMessage({ method: "subscribe", params: { chain_ids: chains, @@ -237,14 +291,41 @@ export class Client { }); } + async submitOpportunityBidViaWebsocket(bid: OpportunityBid): Promise { + const result = await this.sendWebsocketMessage({ + method: "post_liquidation_bid", + params: { + opportunity_bid: this.toServerOpportunityBid(bid), + opportunity_id: bid.opportunityId, + }, + }); + if (result === null) { + throw new Error("Empty response in websocket for bid submission"); + } + return result.id; + } + + async submitBidViaWebsocket(bid: Bid): Promise { + const result = await this.sendWebsocketMessage({ + method: "post_bid", + params: { + bid: this.toServerBid(bid), + }, + }); + if (result === null) { + throw new Error("Empty response in websocket for bid submission"); + } + return result.id; + } + /** * Unsubscribes from the specified chains * * The opportunity handler will no longer be called for opportunities on the specified chains * @param chains */ - async unsubscribeChains(chains: string[]) { - return this.sendWebsocketMessage({ + async unsubscribeChains(chains: string[]): Promise { + await this.sendWebsocketMessage({ method: "unsubscribe", params: { chain_ids: chains, @@ -254,7 +335,7 @@ export class Client { async sendWebsocketMessage( msg: components["schemas"]["ClientMessage"] - ): Promise { + ): Promise { const msg_with_id: components["schemas"]["ClientRequest"] = { ...msg, id: (this.idCounter++).toString(), @@ -262,7 +343,7 @@ export class Client { return new Promise((resolve, reject) => { this.callbackRouter[msg_with_id.id] = (response) => { if (response.status === "success") { - resolve(); + resolve(response.result); } else { reject(response.result); } @@ -409,27 +490,67 @@ export class Client { }; } + private toServerOpportunityBid( + bid: OpportunityBid + ): components["schemas"]["OpportunityBid"] { + return { + amount: bid.bid.amount.toString(), + liquidator: bid.liquidator, + permission_key: bid.permissionKey, + signature: bid.signature, + valid_until: bid.bid.validUntil.toString(), + }; + } + + private toServerBid(bid: Bid): components["schemas"]["Bid"] { + return { + amount: bid.amount.toString(), + calldata: bid.calldata, + chain_id: bid.chainId, + contract: bid.contract, + permission_key: bid.permissionKey, + }; + } + /** * Submits a bid for a liquidation opportunity * @param bid + * @returns The id of the submitted bid, you can use this id to track the status of the bid */ - async submitOpportunityBid(bid: OpportunityBid) { + async submitOpportunityBid(bid: OpportunityBid): Promise { const client = createClient(this.clientOptions); const response = await client.POST( "/v1/liquidation/opportunities/{opportunity_id}/bids", { - body: { - amount: bid.bid.amount.toString(), - liquidator: bid.liquidator, - permission_key: bid.permissionKey, - signature: bid.signature, - valid_until: bid.bid.validUntil.toString(), - }, + body: this.toServerOpportunityBid(bid), params: { path: { opportunity_id: bid.opportunityId } }, } ); if (response.error) { throw new Error(response.error.error); + } else if (response.data === undefined) { + throw new Error("No data returned"); + } else { + return response.data.id; + } + } + + /** + * Submits a raw bid for a permission key + * @param bid + * @returns The id of the submitted bid, you can use this id to track the status of the bid + */ + async submitBid(bid: Bid): Promise { + const client = createClient(this.clientOptions); + const response = await client.POST("/v1/bids", { + body: this.toServerBid(bid), + }); + if (response.error) { + throw new Error(response.error.error); + } else if (response.data === undefined) { + throw new Error("No data returned"); + } else { + return response.data.id; } } } diff --git a/express_relay/sdk/js/src/types.d.ts b/express_relay/sdk/js/src/types.d.ts index 9f5901c62b..d48736d0b5 100644 --- a/express_relay/sdk/js/src/types.d.ts +++ b/express_relay/sdk/js/src/types.d.ts @@ -14,6 +14,13 @@ export interface paths { */ post: operations["bid"]; }; + "/v1/bids/{bid_id}": { + /** + * Query the status of a specific bid. + * @description Query the status of a specific bid. + */ + get: operations["bid_status"]; + }; "/v1/liquidation/opportunities": { /** * Fetch all liquidation opportunities ready to be exectued. @@ -34,7 +41,7 @@ export interface paths { * Bid on liquidation opportunity * @description Bid on liquidation opportunity */ - post: operations["post_bid"]; + post: operations["liquidation_bid"]; }; } @@ -42,6 +49,7 @@ export type webhooks = Record; export interface components { schemas: { + APIResposne: components["schemas"]["BidResult"]; Bid: { /** * @description Amount of bid in wei. @@ -70,21 +78,59 @@ export interface components { permission_key: string; }; BidResult: { + /** + * @description The unique id created to identify the bid. This id can be used to query the status of the bid. + * @example beedbeed-58cc-4372-a567-0e02b2c3d479 + */ + id: string; status: string; }; + BidStatus: + | { + /** @enum {string} */ + status: "pending"; + } + | { + /** + * @description The bid won the auction and was submitted to the chain in a transaction with the given hash + * @example 0x103d4fbd777a36311b5161f2062490f761f25b67406badb2bace62bb170aa4e3 + */ + result: string; + /** @enum {string} */ + status: "submitted"; + } + | { + /** @enum {string} */ + status: "lost"; + }; ClientMessage: | { /** @enum {string} */ method: "subscribe"; params: { - chain_ids: components["schemas"]["ChainId"][]; + chain_ids: string[]; }; } | { /** @enum {string} */ method: "unsubscribe"; params: { - chain_ids: components["schemas"]["ChainId"][]; + chain_ids: string[]; + }; + } + | { + /** @enum {string} */ + method: "post_bid"; + params: { + bid: components["schemas"]["Bid"]; + }; + } + | { + /** @enum {string} */ + method: "post_liquidation_bid"; + params: { + opportunity_bid: components["schemas"]["OpportunityBid"]; + opportunity_id: string; }; }; ClientRequest: components["schemas"]["ClientMessage"] & { @@ -157,16 +203,25 @@ export interface components { value: string; }; /** @description Similar to OpportunityParams, but with the opportunity id included. */ - OpportunityParamsWithMetadata: components["schemas"]["OpportunityParams"] & { - creation_time: components["schemas"]["UnixTimestamp"]; + OpportunityParamsWithMetadata: (components["schemas"]["OpportunityParamsV1"] & { + /** @enum {string} */ + version: "v1"; + }) & { + /** + * Format: int64 + * @description Creation time of the opportunity + * @example 1700000000 + */ + creation_time: number; /** * @description The opportunity unique id - * @example f47ac10b-58cc-4372-a567-0e02b2c3d479 + * @example obo3ee3e-58cc-4372-a567-0e02b2c3d479 */ opportunity_id: string; }; ServerResultMessage: | { + result: components["schemas"]["APIResposne"] | null; /** @enum {string} */ status: "success"; } @@ -183,11 +238,18 @@ export interface components { id?: string | null; }; /** @description This enum is used to send an update to the client for any subscriptions made */ - ServerUpdateResponse: { - opportunity: components["schemas"]["OpportunityParamsWithMetadata"]; - /** @enum {string} */ - type: "new_opportunity"; - }; + ServerUpdateResponse: + | { + opportunity: components["schemas"]["OpportunityParamsWithMetadata"]; + /** @enum {string} */ + type: "new_opportunity"; + } + | { + id: components["schemas"]["BidId"]; + status: components["schemas"]["BidStatus"]; + /** @enum {string} */ + type: "bid_status_update"; + }; TokenQty: { /** * @description Token amount @@ -205,6 +267,11 @@ export interface components { BidResult: { content: { "application/json": { + /** + * @description The unique id created to identify the bid. This id can be used to query the status of the bid. + * @example beedbeed-58cc-4372-a567-0e02b2c3d479 + */ + id: string; status: string; }; }; @@ -220,11 +287,19 @@ export interface components { /** @description Similar to OpportunityParams, but with the opportunity id included. */ OpportunityParamsWithMetadata: { content: { - "application/json": components["schemas"]["OpportunityParams"] & { - creation_time: components["schemas"]["UnixTimestamp"]; + "application/json": (components["schemas"]["OpportunityParamsV1"] & { + /** @enum {string} */ + version: "v1"; + }) & { + /** + * Format: int64 + * @description Creation time of the opportunity + * @example 1700000000 + */ + creation_time: number; /** * @description The opportunity unique id - * @example f47ac10b-58cc-4372-a567-0e02b2c3d479 + * @example obo3ee3e-58cc-4372-a567-0e02b2c3d479 */ opportunity_id: string; }; @@ -256,7 +331,7 @@ export interface operations { }; }; responses: { - /** @description Bid was placed succesfully */ + /** @description Bid was placed successfully */ 200: { content: { "application/json": components["schemas"]["BidResult"]; @@ -271,6 +346,33 @@ export interface operations { }; }; }; + /** + * Query the status of a specific bid. + * @description Query the status of a specific bid. + */ + bid_status: { + parameters: { + path: { + /** @description Bid id to query for */ + bid_id: string; + }; + }; + responses: { + /** @description Latest status of the bid */ + 200: { + content: { + "application/json": components["schemas"]["BidStatus"]; + }; + }; + 400: components["responses"]["ErrorBodyResponse"]; + /** @description Bid was not found */ + 404: { + content: { + "application/json": components["schemas"]["ErrorBodyResponse"]; + }; + }; + }; + }; /** * Fetch all liquidation opportunities ready to be exectued. * @description Fetch all liquidation opportunities ready to be exectued. @@ -286,7 +388,7 @@ export interface operations { /** @description Array of liquidation opportunities ready for bidding */ 200: { content: { - "application/json": components["schemas"]["OpportunityParamsWithId"][]; + "application/json": components["schemas"]["OpportunityParamsWithMetadata"][]; }; }; 400: components["responses"]["ErrorBodyResponse"]; @@ -315,7 +417,7 @@ export interface operations { /** @description The created opportunity */ 200: { content: { - "application/json": components["schemas"]["OpportunityParamsWithId"]; + "application/json": components["schemas"]["OpportunityParamsWithMetadata"]; }; }; 400: components["responses"]["ErrorBodyResponse"]; @@ -331,7 +433,7 @@ export interface operations { * Bid on liquidation opportunity * @description Bid on liquidation opportunity */ - post_bid: { + liquidation_bid: { parameters: { path: { /** @description Opportunity id to bid on */ diff --git a/package-lock.json b/package-lock.json index 3450953715..e38cb7d0b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -613,7 +613,7 @@ }, "express_relay/sdk/js": { "name": "@pythnetwork/express-relay-evm-js", - "version": "0.1.1", + "version": "0.1.2", "license": "Apache-2.0", "dependencies": { "isomorphic-ws": "^5.0.0", From d87ac1a0f47c6259f7916466151299e215e5c51d Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Thu, 29 Feb 2024 13:42:42 +0100 Subject: [PATCH 2/9] Add custom error class for easier excpetion handling --- express_relay/sdk/js/src/index.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/express_relay/sdk/js/src/index.ts b/express_relay/sdk/js/src/index.ts index 0a87204fe2..8ddc38cca1 100644 --- a/express_relay/sdk/js/src/index.ts +++ b/express_relay/sdk/js/src/index.ts @@ -13,6 +13,8 @@ import { import { privateKeyToAccount, sign, signatureToHex } from "viem/accounts"; import WebSocket from "isomorphic-ws"; +export class ClientError extends Error {} + /** * ERC20 token with contract address and amount */ @@ -143,14 +145,14 @@ export function checkHex(hex: string): Hex { if (isHex(hex)) { return hex; } - throw new Error(`Invalid hex: ${hex}`); + throw new ClientError(`Invalid hex: ${hex}`); } export function checkAddress(address: string): Address { if (isAddress(address)) { return address; } - throw new Error(`Invalid address: ${address}`); + throw new ClientError(`Invalid address: ${address}`); } function checkTokenQty(token: { contract: string; amount: string }): TokenQty { @@ -281,7 +283,7 @@ export class Client { */ async subscribeChains(chains: string[]): Promise { if (this.websocketOpportunityCallback === undefined) { - throw new Error("Opportunity handler not set"); + throw new ClientError("Opportunity handler not set"); } await this.sendWebsocketMessage({ method: "subscribe", @@ -300,7 +302,7 @@ export class Client { }, }); if (result === null) { - throw new Error("Empty response in websocket for bid submission"); + throw new ClientError("Empty response in websocket for bid submission"); } return result.id; } @@ -313,7 +315,7 @@ export class Client { }, }); if (result === null) { - throw new Error("Empty response in websocket for bid submission"); + throw new ClientError("Empty response in websocket for bid submission"); } return result.id; } @@ -379,7 +381,7 @@ export class Client { params: { query: { chain_id: chainId } }, }); if (opportunities.data === undefined) { - throw new Error("No opportunities found"); + throw new ClientError("No opportunities found"); } return opportunities.data.flatMap((opportunity) => { const convertedOpportunity = this.convertOpportunity(opportunity); @@ -415,7 +417,7 @@ export class Client { }, }); if (response.error) { - throw new Error(response.error.error); + throw new ClientError(response.error.error); } } @@ -527,9 +529,9 @@ export class Client { } ); if (response.error) { - throw new Error(response.error.error); + throw new ClientError(response.error.error); } else if (response.data === undefined) { - throw new Error("No data returned"); + throw new ClientError("No data returned"); } else { return response.data.id; } @@ -546,9 +548,9 @@ export class Client { body: this.toServerBid(bid), }); if (response.error) { - throw new Error(response.error.error); + throw new ClientError(response.error.error); } else if (response.data === undefined) { - throw new Error("No data returned"); + throw new ClientError("No data returned"); } else { return response.data.id; } From 78073d4c1e70ca272f1a2b3368f623417ec3745b Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Thu, 29 Feb 2024 17:26:05 +0100 Subject: [PATCH 3/9] Address comments --- .../sdk/js/src/examples/SimpleSearcher.ts | 104 ++++++++++-------- express_relay/sdk/js/src/index.ts | 1 + 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/express_relay/sdk/js/src/examples/SimpleSearcher.ts b/express_relay/sdk/js/src/examples/SimpleSearcher.ts index 254d2d948e..bc54a77714 100644 --- a/express_relay/sdk/js/src/examples/SimpleSearcher.ts +++ b/express_relay/sdk/js/src/examples/SimpleSearcher.ts @@ -1,9 +1,67 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -import { checkHex, Client } from "../index"; +import { BidStatusUpdate, checkHex, Client, Opportunity } from "../index"; import { privateKeyToAccount } from "viem/accounts"; import { isHex } from "viem"; +const DAY_IN_SECONDS = 60 * 60 * 24; + +class SimpleSearcher { + constructor( + public client: Client, + public chainId: string, + public privateKey: string + ) { + this.client.setBidStatusHandler(this.bidStatusHandler.bind(this)); + this.client.setOpportunityHandler(this.opportunityHandler.bind(this)); + } + + async bidStatusHandler(bidStatus: BidStatusUpdate) { + console.log( + `Bid status for bid ${bidStatus.id}: ${bidStatus.status.status}` + ); + } + + async opportunityHandler(opportunity: 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 this.client.signOpportunityBid( + opportunity, + bidInfo, + checkHex(argv.privateKey) + ); + try { + const bidId = await this.client.submitOpportunityBidViaWebsocket( + opportunityBid + ); + console.log( + `Successful bid. Opportunity id ${opportunityBid.opportunityId} Bid id ${bidId}` + ); + } catch (error) { + console.error( + `Failed to bid on opportunity ${opportunity.opportunityId}: ${error}` + ); + } + } + + async start() { + try { + await this.client.subscribeChains([argv.chainId]); + console.log( + `Subscribed to chain ${argv.chainId}. Waiting for opportunities...` + ); + } catch (error) { + console.error(error); + this.client.websocket?.close(); + } + } +} + const argv = yargs(hideBin(process.argv)) .option("endpoint", { description: @@ -30,7 +88,6 @@ const argv = yargs(hideBin(process.argv)) .help() .alias("help", "h") .parseSync(); - async function run() { const client = new Client({ baseUrl: argv.endpoint }); if (isHex(argv.privateKey)) { @@ -39,47 +96,8 @@ async function run() { } else { throw new Error(`Invalid private key: ${argv.privateKey}`); } - const DAY_IN_SECONDS = 60 * 60 * 24; - client.setBidStatusHandler(async (bidStatus) => { - console.log( - `Bid status for bid ${bidStatus.id}: ${bidStatus.status.status}` - ); - }); - 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, - checkHex(argv.privateKey) - ); - try { - const bidId = await client.submitOpportunityBidViaWebsocket( - opportunityBid - ); - console.log( - `Successful bid. Opportunity id ${opportunityBid.opportunityId} Bid id ${bidId}` - ); - } 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(); - } + const searcher = new SimpleSearcher(client, argv.chainId, argv.privateKey); + await searcher.start(); } run(); diff --git a/express_relay/sdk/js/src/index.ts b/express_relay/sdk/js/src/index.ts index 8ddc38cca1..c5782fa59b 100644 --- a/express_relay/sdk/js/src/index.ts +++ b/express_relay/sdk/js/src/index.ts @@ -224,6 +224,7 @@ export class Client { await this.websocketBidStatusCallback(message); } } else if ("id" in message && message.id) { + // Response to a request sent earlier via the websocket with the same id const callback = this.callbackRouter[message.id]; if (callback !== undefined) { callback(message); From 8f2535c73d01d1687408ba6ab31edad055f93875 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Wed, 6 Mar 2024 18:21:11 +0100 Subject: [PATCH 4/9] Address more comments --- express_relay/sdk/js/README.md | 13 +++++--- .../sdk/js/src/examples/SimpleSearcher.ts | 18 ++++++++--- express_relay/sdk/js/src/index.ts | 31 ++++++++----------- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/express_relay/sdk/js/README.md b/express_relay/sdk/js/README.md index b480bc7bd3..e154cfa147 100644 --- a/express_relay/sdk/js/README.md +++ b/express_relay/sdk/js/README.md @@ -33,14 +33,12 @@ import { BidInfo, } from "@pythnetwork/express-relay-evm-js"; -const client = new Client({ baseUrl: "https://per-staging.dourolabs.app/" }); - function calculateOpportunityBid(opportunity: Opportunity): BidInfo | null { // searcher implementation here // if the opportunity is not suitable for the searcher, return null } -client.setOpportunityHandler(async (opportunity: Opportunity) => { +async function opportunityCallback(opportunity: Opportunity) { const bidInfo = calculateOpportunityBid(opportunity); if (bidInfo === null) return; const opportunityBid = await client.signOpportunityBid( @@ -49,7 +47,14 @@ client.setOpportunityHandler(async (opportunity: Opportunity) => { privateKey // searcher private key with appropriate permissions and assets ); await client.submitOpportunityBid(opportunityBid); -}); +} + +const client = new Client( + { baseUrl: "https://per-staging.dourolabs.app/" }, + undefined, + opportunityCallback +); + await client.subscribeChains([chain_id]); // chain id you want to subscribe to ``` diff --git a/express_relay/sdk/js/src/examples/SimpleSearcher.ts b/express_relay/sdk/js/src/examples/SimpleSearcher.ts index bc54a77714..d96db6b1df 100644 --- a/express_relay/sdk/js/src/examples/SimpleSearcher.ts +++ b/express_relay/sdk/js/src/examples/SimpleSearcher.ts @@ -7,13 +7,18 @@ import { isHex } from "viem"; const DAY_IN_SECONDS = 60 * 60 * 24; class SimpleSearcher { + private client: Client; constructor( - public client: Client, + public endpoint: string, public chainId: string, public privateKey: string ) { - this.client.setBidStatusHandler(this.bidStatusHandler.bind(this)); - this.client.setOpportunityHandler(this.opportunityHandler.bind(this)); + this.client = new Client( + { baseUrl: endpoint }, + undefined, + this.opportunityHandler.bind(this), + this.bidStatusHandler.bind(this) + ); } async bidStatusHandler(bidStatus: BidStatusUpdate) { @@ -89,14 +94,17 @@ const argv = yargs(hideBin(process.argv)) .alias("help", "h") .parseSync(); async function run() { - const client = new Client({ baseUrl: argv.endpoint }); if (isHex(argv.privateKey)) { const account = privateKeyToAccount(argv.privateKey); console.log(`Using account: ${account.address}`); } else { throw new Error(`Invalid private key: ${argv.privateKey}`); } - const searcher = new SimpleSearcher(client, argv.chainId, argv.privateKey); + const searcher = new SimpleSearcher( + argv.endpoint, + argv.chainId, + argv.privateKey + ); await searcher.start(); } diff --git a/express_relay/sdk/js/src/index.ts b/express_relay/sdk/js/src/index.ts index c5782fa59b..3baf344e3e 100644 --- a/express_relay/sdk/js/src/index.ts +++ b/express_relay/sdk/js/src/index.ts @@ -192,9 +192,16 @@ export class Client { statusUpdate: BidStatusUpdate ) => Promise; - constructor(clientOptions: ClientOptions, wsOptions?: WsOptions) { + constructor( + clientOptions: ClientOptions, + wsOptions?: WsOptions, + opportunityCallback?: (opportunity: Opportunity) => Promise, + bidStatusCallback?: (statusUpdate: BidStatusUpdate) => Promise + ) { this.clientOptions = clientOptions; this.wsOptions = { ...DEFAULT_WS_OPTIONS, ...wsOptions }; + this.websocketOpportunityCallback = opportunityCallback; + this.websocketBidStatusCallback = bidStatusCallback; } private connectWebsocket() { @@ -263,18 +270,6 @@ export class Client { }; } - public setOpportunityHandler( - callback: (opportunity: Opportunity) => Promise - ) { - this.websocketOpportunityCallback = callback; - } - - public setBidStatusHandler( - callback: (statusUpdate: BidStatusUpdate) => Promise - ) { - this.websocketBidStatusCallback = callback; - } - /** * Subscribes to the specified chains * @@ -286,7 +281,7 @@ export class Client { if (this.websocketOpportunityCallback === undefined) { throw new ClientError("Opportunity handler not set"); } - await this.sendWebsocketMessage({ + await this.requestViaWebsocket({ method: "subscribe", params: { chain_ids: chains, @@ -295,7 +290,7 @@ export class Client { } async submitOpportunityBidViaWebsocket(bid: OpportunityBid): Promise { - const result = await this.sendWebsocketMessage({ + const result = await this.requestViaWebsocket({ method: "post_liquidation_bid", params: { opportunity_bid: this.toServerOpportunityBid(bid), @@ -309,7 +304,7 @@ export class Client { } async submitBidViaWebsocket(bid: Bid): Promise { - const result = await this.sendWebsocketMessage({ + const result = await this.requestViaWebsocket({ method: "post_bid", params: { bid: this.toServerBid(bid), @@ -328,7 +323,7 @@ export class Client { * @param chains */ async unsubscribeChains(chains: string[]): Promise { - await this.sendWebsocketMessage({ + await this.requestViaWebsocket({ method: "unsubscribe", params: { chain_ids: chains, @@ -336,7 +331,7 @@ export class Client { }); } - async sendWebsocketMessage( + async requestViaWebsocket( msg: components["schemas"]["ClientMessage"] ): Promise { const msg_with_id: components["schemas"]["ClientRequest"] = { From 293329172850bbbe45fb3b95a61e64017aeb017f Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Fri, 8 Mar 2024 16:06:46 +0100 Subject: [PATCH 5/9] Merge websocket/http apis inside a single function --- express_relay/sdk/js/README.md | 6 +- .../sdk/js/src/examples/SimpleSearcher.ts | 4 +- express_relay/sdk/js/src/index.ts | 107 ++++++++++-------- 3 files changed, 63 insertions(+), 54 deletions(-) diff --git a/express_relay/sdk/js/README.md b/express_relay/sdk/js/README.md index e154cfa147..e3890da90e 100644 --- a/express_relay/sdk/js/README.md +++ b/express_relay/sdk/js/README.md @@ -38,6 +38,10 @@ function calculateOpportunityBid(opportunity: Opportunity): BidInfo | null { // if the opportunity is not suitable for the searcher, return null } +async function bidStatusCallback(bidStatus: BidStatusUpdate) { + console.log(`Bid status for bid ${bidStatus.id}: ${bidStatus.status.status}`); +} + async function opportunityCallback(opportunity: Opportunity) { const bidInfo = calculateOpportunityBid(opportunity); if (bidInfo === null) return; @@ -51,7 +55,7 @@ async function opportunityCallback(opportunity: Opportunity) { const client = new Client( { baseUrl: "https://per-staging.dourolabs.app/" }, - undefined, + bidStatusCallback, opportunityCallback ); diff --git a/express_relay/sdk/js/src/examples/SimpleSearcher.ts b/express_relay/sdk/js/src/examples/SimpleSearcher.ts index d96db6b1df..9af754d00b 100644 --- a/express_relay/sdk/js/src/examples/SimpleSearcher.ts +++ b/express_relay/sdk/js/src/examples/SimpleSearcher.ts @@ -41,9 +41,7 @@ class SimpleSearcher { checkHex(argv.privateKey) ); try { - const bidId = await this.client.submitOpportunityBidViaWebsocket( - opportunityBid - ); + const bidId = await this.client.submitOpportunityBid(opportunityBid); console.log( `Successful bid. Opportunity id ${opportunityBid.opportunityId} Bid id ${bidId}` ); diff --git a/express_relay/sdk/js/src/index.ts b/express_relay/sdk/js/src/index.ts index 3baf344e3e..abe25c8acc 100644 --- a/express_relay/sdk/js/src/index.ts +++ b/express_relay/sdk/js/src/index.ts @@ -289,33 +289,6 @@ export class Client { }); } - async submitOpportunityBidViaWebsocket(bid: OpportunityBid): Promise { - const result = await this.requestViaWebsocket({ - method: "post_liquidation_bid", - params: { - opportunity_bid: this.toServerOpportunityBid(bid), - opportunity_id: bid.opportunityId, - }, - }); - if (result === null) { - throw new ClientError("Empty response in websocket for bid submission"); - } - return result.id; - } - - async submitBidViaWebsocket(bid: Bid): Promise { - const result = await this.requestViaWebsocket({ - method: "post_bid", - params: { - bid: this.toServerBid(bid), - }, - }); - if (result === null) { - throw new ClientError("Empty response in websocket for bid submission"); - } - return result.id; - } - /** * Unsubscribes from the specified chains * @@ -513,42 +486,76 @@ export class Client { /** * Submits a bid for a liquidation opportunity * @param bid + * @param subscribeToUpdates If true, the client will subscribe to bid status updates via websocket and will call the bid status callback if set * @returns The id of the submitted bid, you can use this id to track the status of the bid */ - async submitOpportunityBid(bid: OpportunityBid): Promise { - const client = createClient(this.clientOptions); - const response = await client.POST( - "/v1/liquidation/opportunities/{opportunity_id}/bids", - { - body: this.toServerOpportunityBid(bid), - params: { path: { opportunity_id: bid.opportunityId } }, + async submitOpportunityBid( + bid: OpportunityBid, + subscribeToUpdates = true + ): Promise { + const serverBid = this.toServerOpportunityBid(bid); + if (subscribeToUpdates) { + const result = await this.requestViaWebsocket({ + method: "post_liquidation_bid", + params: { + opportunity_bid: serverBid, + opportunity_id: bid.opportunityId, + }, + }); + if (result === null) { + throw new ClientError("Empty response in websocket for bid submission"); } - ); - if (response.error) { - throw new ClientError(response.error.error); - } else if (response.data === undefined) { - throw new ClientError("No data returned"); + return result.id; } else { - return response.data.id; + const client = createClient(this.clientOptions); + const response = await client.POST( + "/v1/liquidation/opportunities/{opportunity_id}/bids", + { + body: serverBid, + params: { path: { opportunity_id: bid.opportunityId } }, + } + ); + if (response.error) { + throw new ClientError(response.error.error); + } else if (response.data === undefined) { + throw new ClientError("No data returned"); + } else { + return response.data.id; + } } } /** * Submits a raw bid for a permission key * @param bid + * @param subscribeToUpdates If true, the client will subscribe to bid status updates via websocket and will call the bid status callback if set * @returns The id of the submitted bid, you can use this id to track the status of the bid */ - async submitBid(bid: Bid): Promise { - const client = createClient(this.clientOptions); - const response = await client.POST("/v1/bids", { - body: this.toServerBid(bid), - }); - if (response.error) { - throw new ClientError(response.error.error); - } else if (response.data === undefined) { - throw new ClientError("No data returned"); + async submitBid(bid: Bid, subscribeToUpdates = true): Promise { + const serverBid = this.toServerBid(bid); + if (subscribeToUpdates) { + const result = await this.requestViaWebsocket({ + method: "post_bid", + params: { + bid: serverBid, + }, + }); + if (result === null) { + throw new ClientError("Empty response in websocket for bid submission"); + } + return result.id; } else { - return response.data.id; + const client = createClient(this.clientOptions); + const response = await client.POST("/v1/bids", { + body: serverBid, + }); + if (response.error) { + throw new ClientError(response.error.error); + } else if (response.data === undefined) { + throw new ClientError("No data returned"); + } else { + return response.data.id; + } } } } From 055ecf362d7d2bf5e487fa97317e4f7350869f8e Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Fri, 8 Mar 2024 16:18:33 +0100 Subject: [PATCH 6/9] Rename bidInfo->bidParams --- express_relay/sdk/js/README.md | 10 +++++----- .../sdk/js/src/examples/SimpleSearcher.ts | 4 ++-- express_relay/sdk/js/src/index.ts | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/express_relay/sdk/js/README.md b/express_relay/sdk/js/README.md index e3890da90e..637e53897a 100644 --- a/express_relay/sdk/js/README.md +++ b/express_relay/sdk/js/README.md @@ -30,10 +30,10 @@ npm run generate-api-types import { Client, OpportunityParams, - BidInfo, + BidParams, } from "@pythnetwork/express-relay-evm-js"; -function calculateOpportunityBid(opportunity: Opportunity): BidInfo | null { +function calculateOpportunityBid(opportunity: Opportunity): BidParams | null { // searcher implementation here // if the opportunity is not suitable for the searcher, return null } @@ -43,11 +43,11 @@ async function bidStatusCallback(bidStatus: BidStatusUpdate) { } async function opportunityCallback(opportunity: Opportunity) { - const bidInfo = calculateOpportunityBid(opportunity); - if (bidInfo === null) return; + const bidParams = calculateOpportunityBid(opportunity); + if (bidParams === null) return; const opportunityBid = await client.signOpportunityBid( opportunity, - bidInfo, + bidParams, privateKey // searcher private key with appropriate permissions and assets ); await client.submitOpportunityBid(opportunityBid); diff --git a/express_relay/sdk/js/src/examples/SimpleSearcher.ts b/express_relay/sdk/js/src/examples/SimpleSearcher.ts index 9af754d00b..a42999c237 100644 --- a/express_relay/sdk/js/src/examples/SimpleSearcher.ts +++ b/express_relay/sdk/js/src/examples/SimpleSearcher.ts @@ -31,13 +31,13 @@ class SimpleSearcher { 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 = { + const bidParams = { amount: bid, validUntil: BigInt(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)), }; const opportunityBid = await this.client.signOpportunityBid( opportunity, - bidInfo, + bidParams, checkHex(argv.privateKey) ); try { diff --git a/express_relay/sdk/js/src/index.ts b/express_relay/sdk/js/src/index.ts index abe25c8acc..fdb80f62de 100644 --- a/express_relay/sdk/js/src/index.ts +++ b/express_relay/sdk/js/src/index.ts @@ -27,9 +27,9 @@ export type BidId = string; export type ChainId = string; /** - * Bid information + * Bid parameters */ -export type BidInfo = { +export type BidParams = { /** * Bid amount in wei */ @@ -101,7 +101,7 @@ export type OpportunityBid = { */ signature: Hex; - bid: BidInfo; + bid: BidParams; }; /** @@ -393,12 +393,12 @@ export class Client { /** * Creates a signed bid for a liquidation opportunity * @param opportunity Opportunity to bid on - * @param bidInfo Bid amount and valid until timestamp + * @param bidParams Bid amount and valid until timestamp * @param privateKey Private key to sign the bid with */ async signOpportunityBid( opportunity: Opportunity, - bidInfo: BidInfo, + bidParams: BidParams, privateKey: Hex ): Promise { const account = privateKeyToAccount(privateKey); @@ -444,8 +444,8 @@ export class Client { opportunity.contract, opportunity.calldata, opportunity.value, - bidInfo.amount, - bidInfo.validUntil, + bidParams.amount, + bidParams.validUntil, ] ); @@ -454,7 +454,7 @@ export class Client { const hash = signatureToHex(await sign({ hash: msgHash, privateKey })); return { permissionKey: opportunity.permissionKey, - bid: bidInfo, + bid: bidParams, liquidator: account.address, signature: hash, opportunityId: opportunity.opportunityId, From b1ad2d3942886d949338877ca34151917799e205 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Fri, 8 Mar 2024 16:29:14 +0100 Subject: [PATCH 7/9] Adapt to new names on the server --- express_relay/sdk/js/src/index.ts | 38 ++++++++++++++--------------- express_relay/sdk/js/src/types.d.ts | 10 ++++---- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/express_relay/sdk/js/src/index.ts b/express_relay/sdk/js/src/index.ts index fdb80f62de..40aa0e9d31 100644 --- a/express_relay/sdk/js/src/index.ts +++ b/express_relay/sdk/js/src/index.ts @@ -18,8 +18,8 @@ export class ClientError extends Error {} /** * ERC20 token with contract address and amount */ -export type TokenQty = { - contract: Address; +export type TokenAmount = { + token: Address; amount: bigint; }; @@ -73,11 +73,11 @@ export type Opportunity = { /** * Tokens required to repay the debt */ - repayTokens: TokenQty[]; + sellTokens: TokenAmount[]; /** * Tokens to receive after the liquidation */ - receiptTokens: TokenQty[]; + buyTokens: TokenAmount[]; }; /** @@ -155,9 +155,9 @@ export function checkAddress(address: string): Address { throw new ClientError(`Invalid address: ${address}`); } -function checkTokenQty(token: { contract: string; amount: string }): TokenQty { +function checkTokenQty(token: { token: string; amount: string }): TokenAmount { return { - contract: checkAddress(token.contract), + token: checkAddress(token.token), amount: BigInt(token.amount), }; } @@ -265,8 +265,8 @@ export class Client { contract: checkAddress(opportunity.contract), calldata: checkHex(opportunity.calldata), value: BigInt(opportunity.value), - repayTokens: opportunity.repay_tokens.map(checkTokenQty), - receiptTokens: opportunity.receipt_tokens.map(checkTokenQty), + sellTokens: opportunity.sell_tokens.map(checkTokenQty), + buyTokens: opportunity.buy_tokens.map(checkTokenQty), }; } @@ -375,13 +375,13 @@ export class Client { contract: opportunity.contract, calldata: opportunity.calldata, value: opportunity.value.toString(), - repay_tokens: opportunity.repayTokens.map((token) => ({ - contract: token.contract, - amount: token.amount.toString(), + sell_tokens: opportunity.sellTokens.map(({ token, amount }) => ({ + token, + amount: amount.toString(), })), - receipt_tokens: opportunity.receiptTokens.map((token) => ({ - contract: token.contract, - amount: token.amount.toString(), + buy_tokens: opportunity.buyTokens.map(({ token, amount }) => ({ + token, + amount: amount.toString(), })), }, }); @@ -402,9 +402,9 @@ export class Client { privateKey: Hex ): Promise { const account = privateKeyToAccount(privateKey); - const convertTokenQty = (token: TokenQty): [Hex, bigint] => [ - token.contract, - token.amount, + const convertTokenQty = ({ token, amount }: TokenAmount): [Hex, bigint] => [ + token, + amount, ]; const payload = encodeAbiParameters( [ @@ -439,8 +439,8 @@ export class Client { { name: "validUntil", type: "uint256" }, ], [ - opportunity.repayTokens.map(convertTokenQty), - opportunity.receiptTokens.map(convertTokenQty), + opportunity.sellTokens.map(convertTokenQty), + opportunity.buyTokens.map(convertTokenQty), opportunity.contract, opportunity.calldata, opportunity.value, diff --git a/express_relay/sdk/js/src/types.d.ts b/express_relay/sdk/js/src/types.d.ts index d48736d0b5..c98dcaa7b3 100644 --- a/express_relay/sdk/js/src/types.d.ts +++ b/express_relay/sdk/js/src/types.d.ts @@ -174,6 +174,7 @@ export interface components { * in the receipt_tokens field, and will send the tokens specified in the repay_tokens field. */ OpportunityParamsV1: { + buy_tokens: components["schemas"]["TokenAmount"][]; /** * @description Calldata for the contract call. * @example 0xdeadbeef @@ -194,8 +195,7 @@ export interface components { * @example 0xdeadbeefcafe */ permission_key: string; - receipt_tokens: components["schemas"]["TokenQty"][]; - repay_tokens: components["schemas"]["TokenQty"][]; + sell_tokens: components["schemas"]["TokenAmount"][]; /** * @description The value to send with the contract call. * @example 1 @@ -245,12 +245,12 @@ export interface components { type: "new_opportunity"; } | { - id: components["schemas"]["BidId"]; + id: string; status: components["schemas"]["BidStatus"]; /** @enum {string} */ type: "bid_status_update"; }; - TokenQty: { + TokenAmount: { /** * @description Token amount * @example 1000 @@ -260,7 +260,7 @@ export interface components { * @description Token contract address * @example 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 */ - contract: string; + token: string; }; }; responses: { From 0fcbe37152d03d11c1c5df70567b2b66befe7eb2 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Wed, 13 Mar 2024 16:09:33 +0100 Subject: [PATCH 8/9] Adapt field names --- .../sdk/js/src/examples/SimpleSearcher.ts | 4 +- express_relay/sdk/js/src/index.ts | 91 +++++++------- express_relay/sdk/js/src/types.d.ts | 111 +++++++++--------- 3 files changed, 104 insertions(+), 102 deletions(-) diff --git a/express_relay/sdk/js/src/examples/SimpleSearcher.ts b/express_relay/sdk/js/src/examples/SimpleSearcher.ts index a42999c237..bf9177dc73 100644 --- a/express_relay/sdk/js/src/examples/SimpleSearcher.ts +++ b/express_relay/sdk/js/src/examples/SimpleSearcher.ts @@ -22,9 +22,7 @@ class SimpleSearcher { } async bidStatusHandler(bidStatus: BidStatusUpdate) { - console.log( - `Bid status for bid ${bidStatus.id}: ${bidStatus.status.status}` - ); + console.log(`Bid status for bid ${bidStatus.id}: ${bidStatus.status}`); } async opportunityHandler(opportunity: Opportunity) { diff --git a/express_relay/sdk/js/src/index.ts b/express_relay/sdk/js/src/index.ts index 40aa0e9d31..e08e78490f 100644 --- a/express_relay/sdk/js/src/index.ts +++ b/express_relay/sdk/js/src/index.ts @@ -41,11 +41,11 @@ export type BidParams = { }; /** - * All the parameters necessary to represent a liquidation opportunity + * All the parameters necessary to represent an opportunity */ export type Opportunity = { /** - * The chain id where the liquidation will be executed. + * The chain id where the opportunity will be executed. */ chainId: ChainId; @@ -54,34 +54,33 @@ export type Opportunity = { */ opportunityId: string; /** - * Permission key required for succesful execution of the liquidation. + * Permission key required for successful execution of the opportunity. */ permissionKey: Hex; /** - * Contract address to call for execution of the liquidation. + * Contract address to call for execution of the opportunity. */ - contract: Address; + targetContract: Address; /** - * Calldata for the contract call. + * Calldata for the targetContract call. */ - calldata: Hex; + targetCalldata: Hex; /** - * Value to send with the contract call. + * Value to send with the targetContract call. */ - value: bigint; - + targetCallValue: bigint; /** * Tokens required to repay the debt */ sellTokens: TokenAmount[]; /** - * Tokens to receive after the liquidation + * Tokens to receive after the opportunity is executed */ buyTokens: TokenAmount[]; }; /** - * Represents a bid for a liquidation opportunity + * Represents a bid for an opportunity */ export type OpportunityBid = { /** @@ -89,15 +88,15 @@ export type OpportunityBid = { */ opportunityId: string; /** - * The permission key required for succesful execution of the liquidation. + * The permission key required for successful execution of the opportunity. */ permissionKey: Hex; /** - * Liquidator address + * Executor address */ - liquidator: Address; + executor: Address; /** - * Signature of the liquidator + * Signature of the executor */ signature: Hex; @@ -120,26 +119,25 @@ export type Bid = { */ amount: bigint; /** - * @description Calldata for the contract call. + * @description Calldata for the targetContract call. * @example 0xdeadbeef */ - calldata: Hex; + targetCalldata: Hex; /** * @description The chain id to bid on. * @example sepolia */ chainId: ChainId; /** - * @description The contract address to call. + * @description The targetContract address to call. * @example 0xcA11bde05977b3631167028862bE2a173976CA11 */ - contract: Address; + targetContract: Address; }; export type BidStatusUpdate = { id: BidId; - status: components["schemas"]["BidStatus"]; -}; +} & components["schemas"]["BidStatus"]; export function checkHex(hex: string): Hex { if (isHex(hex)) { @@ -228,7 +226,10 @@ export class Client { } } else if ("type" in message && message.type === "bid_status_update") { if (this.websocketBidStatusCallback !== undefined) { - await this.websocketBidStatusCallback(message); + await this.websocketBidStatusCallback({ + id: message.status.id, + ...message.status.bid_status, + }); } } else if ("id" in message && message.id) { // Response to a request sent earlier via the websocket with the same id @@ -262,9 +263,9 @@ export class Client { 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), + targetContract: checkAddress(opportunity.target_contract), + targetCalldata: checkHex(opportunity.target_calldata), + targetCallValue: BigInt(opportunity.target_call_value), sellTokens: opportunity.sell_tokens.map(checkTokenQty), buyTokens: opportunity.buy_tokens.map(checkTokenQty), }; @@ -306,7 +307,7 @@ export class Client { async requestViaWebsocket( msg: components["schemas"]["ClientMessage"] - ): Promise { + ): Promise { const msg_with_id: components["schemas"]["ClientRequest"] = { ...msg, id: (this.idCounter++).toString(), @@ -341,12 +342,12 @@ export class Client { } /** - * Fetches liquidation opportunities + * Fetches opportunities * @param chainId Chain id to fetch opportunities for. e.g: sepolia */ async getOpportunities(chainId?: string): Promise { const client = createClient(this.clientOptions); - const opportunities = await client.GET("/v1/liquidation/opportunities", { + const opportunities = await client.GET("/v1/opportunities", { params: { query: { chain_id: chainId } }, }); if (opportunities.data === undefined) { @@ -362,19 +363,19 @@ export class Client { } /** - * Submits a liquidation opportunity to be exposed to searchers + * Submits an opportunity to be exposed to searchers * @param opportunity Opportunity to submit */ async submitOpportunity(opportunity: Omit) { const client = createClient(this.clientOptions); - const response = await client.POST("/v1/liquidation/opportunities", { + const response = await client.POST("/v1/opportunities", { body: { chain_id: opportunity.chainId, version: "v1", permission_key: opportunity.permissionKey, - contract: opportunity.contract, - calldata: opportunity.calldata, - value: opportunity.value.toString(), + target_contract: opportunity.targetContract, + target_calldata: opportunity.targetCalldata, + target_call_value: opportunity.targetCallValue.toString(), sell_tokens: opportunity.sellTokens.map(({ token, amount }) => ({ token, amount: amount.toString(), @@ -391,7 +392,7 @@ export class Client { } /** - * Creates a signed bid for a liquidation opportunity + * Creates a signed bid for an opportunity * @param opportunity Opportunity to bid on * @param bidParams Bid amount and valid until timestamp * @param privateKey Private key to sign the bid with @@ -441,9 +442,9 @@ export class Client { [ opportunity.sellTokens.map(convertTokenQty), opportunity.buyTokens.map(convertTokenQty), - opportunity.contract, - opportunity.calldata, - opportunity.value, + opportunity.targetContract, + opportunity.targetCalldata, + opportunity.targetCallValue, bidParams.amount, bidParams.validUntil, ] @@ -455,7 +456,7 @@ export class Client { return { permissionKey: opportunity.permissionKey, bid: bidParams, - liquidator: account.address, + executor: account.address, signature: hash, opportunityId: opportunity.opportunityId, }; @@ -466,7 +467,7 @@ export class Client { ): components["schemas"]["OpportunityBid"] { return { amount: bid.bid.amount.toString(), - liquidator: bid.liquidator, + executor: bid.executor, permission_key: bid.permissionKey, signature: bid.signature, valid_until: bid.bid.validUntil.toString(), @@ -476,15 +477,15 @@ export class Client { private toServerBid(bid: Bid): components["schemas"]["Bid"] { return { amount: bid.amount.toString(), - calldata: bid.calldata, + target_calldata: bid.targetCalldata, chain_id: bid.chainId, - contract: bid.contract, + target_contract: bid.targetContract, permission_key: bid.permissionKey, }; } /** - * Submits a bid for a liquidation opportunity + * Submits a bid for an opportunity * @param bid * @param subscribeToUpdates If true, the client will subscribe to bid status updates via websocket and will call the bid status callback if set * @returns The id of the submitted bid, you can use this id to track the status of the bid @@ -496,7 +497,7 @@ export class Client { const serverBid = this.toServerOpportunityBid(bid); if (subscribeToUpdates) { const result = await this.requestViaWebsocket({ - method: "post_liquidation_bid", + method: "post_opportunity_bid", params: { opportunity_bid: serverBid, opportunity_id: bid.opportunityId, @@ -509,7 +510,7 @@ export class Client { } else { const client = createClient(this.clientOptions); const response = await client.POST( - "/v1/liquidation/opportunities/{opportunity_id}/bids", + "/v1/opportunities/{opportunity_id}/bids", { body: serverBid, params: { path: { opportunity_id: bid.opportunityId } }, diff --git a/express_relay/sdk/js/src/types.d.ts b/express_relay/sdk/js/src/types.d.ts index c98dcaa7b3..e72d7788fb 100644 --- a/express_relay/sdk/js/src/types.d.ts +++ b/express_relay/sdk/js/src/types.d.ts @@ -10,7 +10,7 @@ export interface paths { * @description Bid on a specific permission key for a specific chain. * * Your bid will be simulated and verified by the server. Depending on the outcome of the auction, a transaction - * containing the contract call will be sent to the blockchain expecting the bid amount to be paid after the call. + * containing the targetContract call will be sent to the blockchain expecting the bid amount to be paid after the call. */ post: operations["bid"]; }; @@ -21,27 +21,27 @@ export interface paths { */ get: operations["bid_status"]; }; - "/v1/liquidation/opportunities": { + "/v1/opportunities": { /** - * Fetch all liquidation opportunities ready to be exectued. - * @description Fetch all liquidation opportunities ready to be exectued. + * Fetch all opportunities ready to be exectued. + * @description Fetch all opportunities ready to be exectued. */ get: operations["get_opportunities"]; /** - * Submit a liquidation opportunity ready to be executed. - * @description Submit a liquidation opportunity ready to be executed. + * Submit an opportunity ready to be executed. + * @description Submit an opportunity ready to be executed. * * The opportunity will be verified by the server. If the opportunity is valid, it will be stored in the database * and will be available for bidding. */ post: operations["post_opportunity"]; }; - "/v1/liquidation/opportunities/{opportunity_id}/bids": { + "/v1/opportunities/{opportunity_id}/bids": { /** - * Bid on liquidation opportunity - * @description Bid on liquidation opportunity + * Bid on opportunity + * @description Bid on opportunity */ - post: operations["liquidation_bid"]; + post: operations["opportunity_bid"]; }; } @@ -49,33 +49,33 @@ export type webhooks = Record; export interface components { schemas: { - APIResposne: components["schemas"]["BidResult"]; + APIResponse: components["schemas"]["BidResult"]; Bid: { /** * @description Amount of bid in wei. * @example 10 */ amount: string; - /** - * @description Calldata for the contract call. - * @example 0xdeadbeef - */ - calldata: string; /** * @description The chain id to bid on. * @example sepolia */ chain_id: string; - /** - * @description The contract address to call. - * @example 0xcA11bde05977b3631167028862bE2a173976CA11 - */ - contract: string; /** * @description The permission key to bid on. * @example 0xdeadbeef */ permission_key: string; + /** + * @description Calldata for the targetContract call. + * @example 0xdeadbeef + */ + target_calldata: string; + /** + * @description The targetContract address to call. + * @example 0xcA11bde05977b3631167028862bE2a173976CA11 + */ + target_contract: string; }; BidResult: { /** @@ -103,6 +103,10 @@ export interface components { /** @enum {string} */ status: "lost"; }; + BidStatusWithId: { + bid_status: components["schemas"]["BidStatus"]; + id: string; + }; ClientMessage: | { /** @enum {string} */ @@ -127,7 +131,7 @@ export interface components { } | { /** @enum {string} */ - method: "post_liquidation_bid"; + method: "post_opportunity_bid"; params: { opportunity_bid: components["schemas"]["OpportunityBid"]; opportunity_id: string; @@ -146,10 +150,10 @@ export interface components { */ amount: string; /** - * @description Liquidator address + * @description Executor address * @example 0x5FbDB2315678afecb367f032d93F642f64180aa2 */ - liquidator: string; + executor: string; /** * @description The opportunity permission key * @example 0xdeadbeefcafe @@ -169,38 +173,38 @@ export interface components { }; /** * @description Opportunity parameters needed for on-chain execution - * If a searcher signs the opportunity and have approved enough tokens to liquidation adapter, - * by calling this contract with the given calldata and structures, they will receive the tokens specified - * in the receipt_tokens field, and will send the tokens specified in the repay_tokens field. + * If a searcher signs the opportunity and have approved enough tokens to opportunity adapter, + * by calling this target targetContract with the given target targetCalldata and structures, they will + * send the tokens specified in the sell_tokens field and receive the tokens specified in the buy_tokens field. */ OpportunityParamsV1: { buy_tokens: components["schemas"]["TokenAmount"][]; /** - * @description Calldata for the contract call. - * @example 0xdeadbeef - */ - calldata: string; - /** - * @description The chain id where the liquidation will be executed. + * @description The chain id where the opportunity will be executed. * @example sepolia */ chain_id: string; /** - * @description The contract address to call for execution of the liquidation. - * @example 0xcA11bde05977b3631167028862bE2a173976CA11 - */ - contract: string; - /** - * @description The permission key required for succesful execution of the liquidation. + * @description The permission key required for successful execution of the opportunity. * @example 0xdeadbeefcafe */ permission_key: string; sell_tokens: components["schemas"]["TokenAmount"][]; /** - * @description The value to send with the contract call. + * @description The targetCallValue to send with the targetContract call. * @example 1 */ - value: string; + target_call_value: string; + /** + * @description Calldata for the target targetContract call. + * @example 0xdeadbeef + */ + target_calldata: string; + /** + * @description The targetContract address to call for execution of the opportunity. + * @example 0xcA11bde05977b3631167028862bE2a173976CA11 + */ + target_contract: string; }; /** @description Similar to OpportunityParams, but with the opportunity id included. */ OpportunityParamsWithMetadata: (components["schemas"]["OpportunityParamsV1"] & { @@ -221,7 +225,7 @@ export interface components { }; ServerResultMessage: | { - result: components["schemas"]["APIResposne"] | null; + result: components["schemas"]["APIResponse"] | null; /** @enum {string} */ status: "success"; } @@ -245,8 +249,7 @@ export interface components { type: "new_opportunity"; } | { - id: string; - status: components["schemas"]["BidStatus"]; + status: components["schemas"]["BidStatusWithId"]; /** @enum {string} */ type: "bid_status_update"; }; @@ -257,7 +260,7 @@ export interface components { */ amount: string; /** - * @description Token contract address + * @description Token targetContract address * @example 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 */ token: string; @@ -322,7 +325,7 @@ export interface operations { * @description Bid on a specific permission key for a specific chain. * * Your bid will be simulated and verified by the server. Depending on the outcome of the auction, a transaction - * containing the contract call will be sent to the blockchain expecting the bid amount to be paid after the call. + * containing the targetContract call will be sent to the blockchain expecting the bid amount to be paid after the call. */ bid: { requestBody: { @@ -374,8 +377,8 @@ export interface operations { }; }; /** - * Fetch all liquidation opportunities ready to be exectued. - * @description Fetch all liquidation opportunities ready to be exectued. + * Fetch all opportunities ready to be exectued. + * @description Fetch all opportunities ready to be exectued. */ get_opportunities: { parameters: { @@ -385,7 +388,7 @@ export interface operations { }; }; responses: { - /** @description Array of liquidation opportunities ready for bidding */ + /** @description Array of opportunities ready for bidding */ 200: { content: { "application/json": components["schemas"]["OpportunityParamsWithMetadata"][]; @@ -401,8 +404,8 @@ export interface operations { }; }; /** - * Submit a liquidation opportunity ready to be executed. - * @description Submit a liquidation opportunity ready to be executed. + * Submit an opportunity ready to be executed. + * @description Submit an opportunity ready to be executed. * * The opportunity will be verified by the server. If the opportunity is valid, it will be stored in the database * and will be available for bidding. @@ -430,10 +433,10 @@ export interface operations { }; }; /** - * Bid on liquidation opportunity - * @description Bid on liquidation opportunity + * Bid on opportunity + * @description Bid on opportunity */ - liquidation_bid: { + opportunity_bid: { parameters: { path: { /** @description Opportunity id to bid on */ From a5b29723d7e65b57c2d410cd53dd0635b2b0dce5 Mon Sep 17 00:00:00 2001 From: Amin Moghaddam Date: Thu, 14 Mar 2024 20:08:22 +0100 Subject: [PATCH 9/9] Address comments --- express_relay/sdk/js/README.md | 3 +- express_relay/sdk/js/package.json | 6 +- .../{SimpleSearcher.ts => simpleSearcher.ts} | 9 +- express_relay/sdk/js/src/index.ts | 159 +++--------------- .../js/src/{types.d.ts => serverTypes.d.ts} | 0 express_relay/sdk/js/src/types.ts | 124 ++++++++++++++ package-lock.json | 2 +- 7 files changed, 161 insertions(+), 142 deletions(-) rename express_relay/sdk/js/src/examples/{SimpleSearcher.ts => simpleSearcher.ts} (91%) rename express_relay/sdk/js/src/{types.d.ts => serverTypes.d.ts} (100%) create mode 100644 express_relay/sdk/js/src/types.ts diff --git a/express_relay/sdk/js/README.md b/express_relay/sdk/js/README.md index 637e53897a..5f86bf40c3 100644 --- a/express_relay/sdk/js/README.md +++ b/express_relay/sdk/js/README.md @@ -68,14 +68,13 @@ There is an example searcher in [examples](./src/examples/) directory. #### SimpleSearcher -[This example](./src/examples/SimpleSearcher.ts) fetches `OpportunityParams` from the specified endpoint, +[This example](./src/examples/simpleSearcher.ts) fetches `OpportunityParams` from the specified endpoint, creates a fixed bid on each opportunity and signs them with the provided private key, and finally submits them back to the server. You can run it with `npm run simple-searcher`. A full command looks like this: ```bash npm run simple-searcher -- \ --endpoint https://per-staging.dourolabs.app/ \ - --bid 100000 \ --chain-id op_sepolia \ --private-key ``` diff --git a/express_relay/sdk/js/package.json b/express_relay/sdk/js/package.json index a64fa0f4c4..e29a587d56 100644 --- a/express_relay/sdk/js/package.json +++ b/express_relay/sdk/js/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/express-relay-evm-js", - "version": "0.1.2", + "version": "0.2.0", "description": "Utilities for interacting with the express relay protocol", "homepage": "https://pyth.network", "author": { @@ -17,8 +17,8 @@ "scripts": { "build": "tsc", "test": "jest src/ --passWithNoTests", - "simple-searcher": "npm run build && node lib/examples/SimpleSearcher.js", - "generate-api-types": "openapi-typescript http://127.0.0.1:9000/docs/openapi.json --output src/types.d.ts", + "simple-searcher": "npm run build && node lib/examples/simpleSearcher.js", + "generate-api-types": "openapi-typescript http://127.0.0.1:9000/docs/openapi.json --output src/serverTypes.d.ts", "format": "prettier --write \"src/**/*.ts\"", "lint": "eslint src", "prepublishOnly": "npm run build && npm test && npm run lint", diff --git a/express_relay/sdk/js/src/examples/SimpleSearcher.ts b/express_relay/sdk/js/src/examples/simpleSearcher.ts similarity index 91% rename from express_relay/sdk/js/src/examples/SimpleSearcher.ts rename to express_relay/sdk/js/src/examples/simpleSearcher.ts index bf9177dc73..db0c648dbe 100644 --- a/express_relay/sdk/js/src/examples/SimpleSearcher.ts +++ b/express_relay/sdk/js/src/examples/simpleSearcher.ts @@ -1,8 +1,9 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -import { BidStatusUpdate, checkHex, Client, Opportunity } from "../index"; +import { checkHex, Client } from "../index"; import { privateKeyToAccount } from "viem/accounts"; import { isHex } from "viem"; +import { BidStatusUpdate, Opportunity } from "../types"; const DAY_IN_SECONDS = 60 * 60 * 24; @@ -22,7 +23,11 @@ class SimpleSearcher { } async bidStatusHandler(bidStatus: BidStatusUpdate) { - console.log(`Bid status for bid ${bidStatus.id}: ${bidStatus.status}`); + console.log( + `Bid status for bid ${bidStatus.id}: ${bidStatus.status} ${ + bidStatus.status == "submitted" ? bidStatus.result : "" + }` + ); } async opportunityHandler(opportunity: Opportunity) { diff --git a/express_relay/sdk/js/src/index.ts b/express_relay/sdk/js/src/index.ts index e08e78490f..80760303f4 100644 --- a/express_relay/sdk/js/src/index.ts +++ b/express_relay/sdk/js/src/index.ts @@ -1,4 +1,4 @@ -import type { paths, components } from "./types"; +import type { components, paths } from "./serverTypes"; import createClient, { ClientOptions as FetchClientOptions, } from "openapi-fetch"; @@ -12,133 +12,34 @@ import { } from "viem"; import { privateKeyToAccount, sign, signatureToHex } from "viem/accounts"; import WebSocket from "isomorphic-ws"; +import { + Bid, + BidId, + BidParams, + BidStatusUpdate, + Opportunity, + OpportunityBid, + OpportunityParams, + TokenAmount, +} from "./types"; + +export * from "./types"; export class ClientError extends Error {} -/** - * ERC20 token with contract address and amount - */ -export type TokenAmount = { - token: Address; - amount: bigint; -}; - -export type BidId = string; -export type ChainId = string; - -/** - * Bid parameters - */ -export type BidParams = { - /** - * Bid amount in wei - */ - amount: bigint; - /** - * Unix timestamp for when the bid is no longer valid in seconds - */ - validUntil: bigint; -}; - -/** - * All the parameters necessary to represent an opportunity - */ -export type Opportunity = { - /** - * The chain id where the opportunity will be executed. - */ - chainId: ChainId; - - /** - * Unique identifier for the opportunity - */ - opportunityId: string; - /** - * Permission key required for successful execution of the opportunity. - */ - permissionKey: Hex; - /** - * Contract address to call for execution of the opportunity. - */ - targetContract: Address; - /** - * Calldata for the targetContract call. - */ - targetCalldata: Hex; - /** - * Value to send with the targetContract call. - */ - targetCallValue: bigint; - /** - * Tokens required to repay the debt - */ - sellTokens: TokenAmount[]; - /** - * Tokens to receive after the opportunity is executed - */ - buyTokens: TokenAmount[]; -}; +type ClientOptions = FetchClientOptions & { baseUrl: string }; -/** - * Represents a bid for an opportunity - */ -export type OpportunityBid = { - /** - * Opportunity unique identifier in uuid format - */ - opportunityId: string; - /** - * The permission key required for successful execution of the opportunity. - */ - permissionKey: Hex; - /** - * Executor address - */ - executor: Address; +export interface WsOptions { /** - * Signature of the executor + * Max time to wait for a response from the server in milliseconds */ - signature: Hex; - - bid: BidParams; -}; + response_timeout: number; +} -/** - * Represents a raw bid on acquiring a permission key - */ -export type Bid = { - /** - * The permission key to bid on - * @example 0xc0ffeebabe - * - */ - permissionKey: Hex; - /** - * @description Amount of bid in wei. - * @example 10 - */ - amount: bigint; - /** - * @description Calldata for the targetContract call. - * @example 0xdeadbeef - */ - targetCalldata: Hex; - /** - * @description The chain id to bid on. - * @example sepolia - */ - chainId: ChainId; - /** - * @description The targetContract address to call. - * @example 0xcA11bde05977b3631167028862bE2a173976CA11 - */ - targetContract: Address; +const DEFAULT_WS_OPTIONS: WsOptions = { + response_timeout: 5000, }; -export type BidStatusUpdate = { - id: BidId; -} & components["schemas"]["BidStatus"]; - export function checkHex(hex: string): Hex { if (isHex(hex)) { return hex; @@ -153,26 +54,16 @@ export function checkAddress(address: string): Address { throw new ClientError(`Invalid address: ${address}`); } -function checkTokenQty(token: { token: string; amount: string }): TokenAmount { +export function checkTokenQty(token: { + token: string; + amount: string; +}): TokenAmount { return { token: checkAddress(token.token), amount: BigInt(token.amount), }; } -type ClientOptions = FetchClientOptions & { baseUrl: string }; - -export interface WsOptions { - /** - * Max time to wait for a response from the server in milliseconds - */ - response_timeout: number; -} - -const DEFAULT_WS_OPTIONS: WsOptions = { - response_timeout: 5000, -}; - export class Client { public clientOptions: ClientOptions; public wsOptions: WsOptions; @@ -366,7 +257,7 @@ export class Client { * Submits an opportunity to be exposed to searchers * @param opportunity Opportunity to submit */ - async submitOpportunity(opportunity: Omit) { + async submitOpportunity(opportunity: OpportunityParams) { const client = createClient(this.clientOptions); const response = await client.POST("/v1/opportunities", { body: { diff --git a/express_relay/sdk/js/src/types.d.ts b/express_relay/sdk/js/src/serverTypes.d.ts similarity index 100% rename from express_relay/sdk/js/src/types.d.ts rename to express_relay/sdk/js/src/serverTypes.d.ts diff --git a/express_relay/sdk/js/src/types.ts b/express_relay/sdk/js/src/types.ts new file mode 100644 index 0000000000..60b8924848 --- /dev/null +++ b/express_relay/sdk/js/src/types.ts @@ -0,0 +1,124 @@ +import { Address, Hex } from "viem"; +import type { components } from "./serverTypes"; + +/** + * ERC20 token with contract address and amount + */ +export type TokenAmount = { + token: Address; + amount: bigint; +}; +export type BidId = string; +export type ChainId = string; +/** + * Bid parameters + */ +export type BidParams = { + /** + * Bid amount in wei + */ + amount: bigint; + /** + * Unix timestamp for when the bid is no longer valid in seconds + */ + validUntil: bigint; +}; +/** + * Represents a valid opportunity ready to be executed + */ +export type Opportunity = { + /** + * The chain id where the opportunity will be executed. + */ + chainId: ChainId; + + /** + * Unique identifier for the opportunity + */ + opportunityId: string; + /** + * Permission key required for successful execution of the opportunity. + */ + permissionKey: Hex; + /** + * Contract address to call for execution of the opportunity. + */ + targetContract: Address; + /** + * Calldata for the targetContract call. + */ + targetCalldata: Hex; + /** + * Value to send with the targetContract call. + */ + targetCallValue: bigint; + /** + * Tokens required to repay the debt + */ + sellTokens: TokenAmount[]; + /** + * Tokens to receive after the opportunity is executed + */ + buyTokens: TokenAmount[]; +}; +/** + * All the parameters necessary to represent an opportunity + */ +export type OpportunityParams = Omit; +/** + * Represents a bid for an opportunity + */ +export type OpportunityBid = { + /** + * Opportunity unique identifier in uuid format + */ + opportunityId: string; + /** + * The permission key required for successful execution of the opportunity. + */ + permissionKey: Hex; + /** + * Executor address + */ + executor: Address; + /** + * Signature of the executor + */ + signature: Hex; + + bid: BidParams; +}; +/** + * Represents a raw bid on acquiring a permission key + */ +export type Bid = { + /** + * The permission key to bid on + * @example 0xc0ffeebabe + * + */ + permissionKey: Hex; + /** + * @description Amount of bid in wei. + * @example 10 + */ + amount: bigint; + /** + * @description Calldata for the targetContract call. + * @example 0xdeadbeef + */ + targetCalldata: Hex; + /** + * @description The chain id to bid on. + * @example sepolia + */ + chainId: ChainId; + /** + * @description The targetContract address to call. + * @example 0xcA11bde05977b3631167028862bE2a173976CA11 + */ + targetContract: Address; +}; +export type BidStatusUpdate = { + id: BidId; +} & components["schemas"]["BidStatus"]; diff --git a/package-lock.json b/package-lock.json index e38cb7d0b8..beca3f9ce3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -613,7 +613,7 @@ }, "express_relay/sdk/js": { "name": "@pythnetwork/express-relay-evm-js", - "version": "0.1.2", + "version": "0.2.0", "license": "Apache-2.0", "dependencies": { "isomorphic-ws": "^5.0.0",