diff --git a/auction-server/api-types/src/opportunity.rs b/auction-server/api-types/src/opportunity.rs index ac3cfc7c..30589a39 100644 --- a/auction-server/api-types/src/opportunity.rs +++ b/auction-server/api-types/src/opportunity.rs @@ -61,9 +61,12 @@ pub struct OpportunityBidResult { } #[derive(Serialize, Deserialize, ToSchema, Clone, PartialEq, Debug, Display)] +#[serde(rename_all = "lowercase")] pub enum ProgramSvm { + #[serde(rename = "swap_kamino")] #[strum(serialize = "swap_kamino")] SwapKamino, + #[serde(rename = "limo")] #[strum(serialize = "limo")] Limo, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5acf0732..f8ec0588 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -271,6 +271,9 @@ importers: "@solana/web3.js": specifier: "catalog:" version: 1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) + bs58: + specifier: ^6.0.0 + version: 6.0.0 decimal.js: specifier: ^10.4.3 version: 10.4.3 diff --git a/sdk/js/package.json b/sdk/js/package.json index e5581ac8..d411d1b8 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/express-relay-js", - "version": "0.17.1", + "version": "0.17.2", "description": "Utilities for interacting with the express relay protocol", "homepage": "https://github.com/pyth-network/per/tree/main/sdk/js", "author": "Douro Labs", @@ -22,7 +22,7 @@ "generate-api-types": "openapi-typescript http://127.0.0.1:9000/docs/openapi.json --output src/serverTypes.d.ts", "generate-anchor-types": "anchor idl type src/idl/idlExpressRelay.json --out src/expressRelayTypes.d.ts && anchor idl type src/examples/idl/idlDummy.json --out src/examples/dummyTypes.d.ts", "format": "prettier --write \"src/**/*.ts\"", - "lint": "eslint src", + "lint": "eslint 'src/**/*.ts' --ignore-pattern '**/*.d.ts'", "prepublishOnly": "pnpm build && pnpm test && pnpm lint", "preversion": "pnpm lint", "version": "pnpm format && git add -A src" @@ -41,6 +41,7 @@ "@coral-xyz/anchor": "catalog:", "@kamino-finance/limo-sdk": "catalog:", "@solana/web3.js": "catalog:", + "bs58": "^6.0.0", "decimal.js": "^10.4.3", "isomorphic-ws": "^5.0.0", "openapi-client-axios": "^7.5.5", diff --git a/sdk/js/src/index.ts b/sdk/js/src/index.ts index 9950823c..cbc30617 100644 --- a/sdk/js/src/index.ts +++ b/sdk/js/src/index.ts @@ -4,6 +4,7 @@ import createClient, { } from "openapi-fetch"; import { Address, Hex, isAddress, isHex } from "viem"; import WebSocket from "isomorphic-ws"; +import base58 from "bs58"; import { Bid, BidId, @@ -20,6 +21,8 @@ import { SvmChainUpdate, OpportunityDelete, ChainType, + QuoteRequest, + QuoteResponse, } from "./types"; import { Connection, @@ -473,6 +476,35 @@ export class Client { } } + /** + * Gets the best quote for a given quote request + * @param quoteRequest Quote request to submit + * @returns Quote response representing the best quote for the request + */ + async getQuote(quoteRequest: QuoteRequest): Promise { + const client = createClient(this.clientOptions); + const body = { + chain_id: quoteRequest.chainId, + input_token_mint: quoteRequest.inputTokenMint.toBase58(), + output_token_mint: quoteRequest.outputTokenMint.toBase58(), + router: quoteRequest.router.toBase58(), + specified_token_amount: quoteRequest.specifiedTokenAmount, + user_wallet_address: quoteRequest.userWallet.toBase58(), + version: "v1" as const, + }; + // TODO: we may want to wrap all the GET/POST calls in a try/catch block to handle errors + const response = await client.POST("/v1/opportunities/quote", { + body: body, + }); + if (response.error) { + throw ClientError.newHttpError( + JSON.stringify(response.error), + response.response.status + ); + } + return this.convertQuoteResponse(response.data); + } + /** * Submits a raw bid for a permission key * @param bid @@ -601,6 +633,29 @@ export class Client { } } + /** + * Converts a quote response from the server to the client format + * @param quoteResponse + * @returns Quote response in the converted client format + */ + public convertQuoteResponse( + quoteResponse: components["schemas"]["QuoteSvm"] + ): QuoteResponse { + return { + chainId: quoteResponse.chain_id, + expirationTime: new Date(quoteResponse.expiration_time * 1000), + inputToken: { + token: new PublicKey(quoteResponse.input_token.token), + amount: BigInt(quoteResponse.input_token.amount), + }, + outputToken: { + token: new PublicKey(quoteResponse.output_token.token), + amount: BigInt(quoteResponse.output_token.amount), + }, + transaction: Transaction.from(base58.decode(quoteResponse.transaction)), + }; + } + // EVM specific functions /** diff --git a/sdk/js/src/serverTypes.d.ts b/sdk/js/src/serverTypes.d.ts index 4d137969..13602705 100644 --- a/sdk/js/src/serverTypes.d.ts +++ b/sdk/js/src/serverTypes.d.ts @@ -3,6 +3,17 @@ * Do not make direct changes to the file. */ +/** OneOf type helpers */ +type Without = { [P in Exclude]?: never }; +type XOR = T | U extends object + ? (Without & U) | (Without & T) + : T | U; +type OneOf = T extends [infer Only] + ? Only + : T extends [infer A, infer B, ...infer Rest] + ? OneOf<[XOR, ...Rest]> + : never; + export interface paths { "/v1/bids": { /** @@ -406,7 +417,7 @@ export interface components { | { /** * @description The Limo order to be executed, encoded in base64. - * @example DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5 + * @example UxMUbQAsjrfQUp5stVwMJ6Mucq7VWTvt4ICe69BJ8lVXqwM+0sysV8OqZTdM0W4p... */ order: string; /** @@ -418,14 +429,8 @@ export interface components { program: "limo"; } | { - /** - * Format: double - * @description The maximum slippage percentage that the user is willing to accept. - * @example 0.5 - */ - maximum_slippage_percentage: number; /** @enum {string} */ - program: "phantom"; + program: "swap"; /** * @description The user wallet address which requested the quote from the wallet. * @example DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5 @@ -479,7 +484,7 @@ export interface components { | { /** * @description The Limo order to be executed, encoded in base64. - * @example DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5 + * @example UxMUbQAsjrfQUp5stVwMJ6Mucq7VWTvt4ICe69BJ8lVXqwM+0sysV8OqZTdM0W4p... */ order: string; /** @@ -491,14 +496,8 @@ export interface components { program: "limo"; } | { - /** - * Format: double - * @description The maximum slippage percentage that the user is willing to accept. - * @example 0.5 - */ - maximum_slippage_percentage: number; /** @enum {string} */ - program: "phantom"; + program: "swap"; /** * @description The user wallet address which requested the quote from the wallet. * @example DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5 @@ -614,7 +613,7 @@ export interface components { | { /** * @description The Limo order to be executed, encoded in base64. - * @example DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5 + * @example UxMUbQAsjrfQUp5stVwMJ6Mucq7VWTvt4ICe69BJ8lVXqwM+0sysV8OqZTdM0W4p... */ order: string; /** @@ -626,26 +625,34 @@ export interface components { program: "limo"; } | { - buy_token: components["schemas"]["TokenAmountSvm"]; - /** - * Format: double - * @description The maximum slippage percentage that the user is willing to accept. - * @example 0.5 - */ - maximum_slippage_percentage: number; /** * @description The permission account to be permitted by the ER contract for the opportunity execution of the protocol. * @example DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5 */ permission_account: string; /** @enum {string} */ - program: "phantom"; + program: "swap"; /** * @description The router account to be used for the opportunity execution of the protocol. * @example DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5 */ router_account: string; - sell_token: components["schemas"]["TokenAmountSvm"]; + tokens: OneOf< + [ + { + InputTokenSpecified: { + input_token: components["schemas"]["TokenAmountSvm"]; + output_token: components["schemas"]["Pubkey"]; + }; + }, + { + OutputTokenSpecified: { + input_token: components["schemas"]["Pubkey"]; + output_token: components["schemas"]["TokenAmountSvm"]; + }; + } + ] + >; /** * @description The user wallet address which requested the quote from the wallet. * @example DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5 @@ -678,59 +685,83 @@ export interface components { slot: number; }; /** @enum {string} */ - ProgramSvm: "phantom" | "limo"; + ProgramSvm: "swap_kamino" | "limo"; Quote: components["schemas"]["QuoteSvm"]; QuoteCreate: components["schemas"]["QuoteCreateSvm"]; + QuoteCreateSvm: components["schemas"]["QuoteCreateV1SvmParams"] & { + /** @enum {string} */ + version: "v1"; + }; /** - * @description Parameters needed to create a new opportunity from the Phantom wallet. + * @description Parameters needed to create a new opportunity from the swap request. * Auction server will extract the output token price for the auction. */ - QuoteCreatePhantomV1Svm: { + QuoteCreateV1SvmParams: { /** * @description The chain id for creating the quote. * @example solana */ chain_id: string; - /** - * Format: int64 - * @description The input token amount that the user wants to swap. - * @example 100 - */ - input_token_amount: number; /** * @description The token mint address of the input token. * @example EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v */ input_token_mint: string; - /** - * Format: double - * @description The maximum slippage percentage that the user is willing to accept. - * @example 0.5 - */ - maximum_slippage_percentage: number; /** * @description The token mint address of the output token. * @example EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v */ output_token_mint: string; + /** + * @description The router account to send referral fees to. + * @example DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5 + */ + router: string; + specified_token_amount: + | { + /** + * Format: int64 + * @example 100 + */ + amount: number; + /** @enum {string} */ + side: "input"; + } + | { + /** + * Format: int64 + * @example 50 + */ + amount: number; + /** @enum {string} */ + side: "output"; + }; /** * @description The user wallet address which requested the quote from the wallet. * @example DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5 */ user_wallet_address: string; }; - QuoteCreateSvm: components["schemas"]["QuoteCreateV1Svm"] & { - /** @enum {string} */ - version: "v1"; - }; - QuoteCreateV1Svm: components["schemas"]["QuoteCreatePhantomV1Svm"] & { - /** @enum {string} */ - program: "phantom"; - }; QuoteSvm: components["schemas"]["QuoteV1Svm"] & { /** @enum {string} */ version: "v1"; }; + QuoteTokens: OneOf< + [ + { + InputTokenSpecified: { + input_token: components["schemas"]["TokenAmountSvm"]; + output_token: components["schemas"]["Pubkey"]; + }; + }, + { + OutputTokenSpecified: { + input_token: components["schemas"]["Pubkey"]; + output_token: components["schemas"]["TokenAmountSvm"]; + }; + } + ] + >; QuoteV1Svm: { /** * @description The chain id for the quote. @@ -744,12 +775,6 @@ export interface components { */ expiration_time: number; input_token: components["schemas"]["TokenAmountSvm"]; - /** - * Format: double - * @description The maximum slippage percentage that the user is willing to accept. - * @example 0.5 - */ - maximum_slippage_percentage: number; output_token: components["schemas"]["TokenAmountSvm"]; /** * @description The signed transaction for the quote to be executed on chain which is valid until the expiration time. @@ -797,6 +822,25 @@ export interface components { /** @enum {string} */ type: "remove_opportunities"; }; + SpecifiedTokenAmount: + | { + /** + * Format: int64 + * @example 100 + */ + amount: number; + /** @enum {string} */ + side: "input"; + } + | { + /** + * Format: int64 + * @example 50 + */ + amount: number; + /** @enum {string} */ + side: "output"; + }; SvmChainUpdate: { /** @example SLxp9LxX1eE9Z5v99Y92DaYEwyukFgMUF6zRerCF12j */ blockhash: string; @@ -824,7 +868,9 @@ export interface components { TokenAmountSvm: { /** * Format: int64 - * @description The token amount in lamports. + * @description The token amount, represented in the smallest unit of the respective token: + * - For Solana, it is measured in lamports. + * - For other tokens, it follows the smallest denomination of that token. * @example 1000 */ amount: number; diff --git a/sdk/js/src/types.ts b/sdk/js/src/types.ts index d3d2520b..8cc2c3ff 100644 --- a/sdk/js/src/types.ts +++ b/sdk/js/src/types.ts @@ -10,6 +10,14 @@ export type TokenAmount = { token: Address; amount: bigint; }; +/** + * SVM token with contract address and amount + */ +export type TokenAmountSvm = { + token: PublicKey; + amount: bigint; +}; + /** * TokenPermissions struct for permit2 */ @@ -278,3 +286,44 @@ export type OpportunityDelete = | (OpportunityDeleteEvm & { chainType: ChainType.EVM; }); + +export type SpecifiedTokenAmount = { + side: "input" | "output"; + amount: number; +}; + +export type QuoteRequest = { + chainId: ChainId; + /** + * @description The mint of the token that the user wants to swap from + * @example So11111111111111111111111111111111111111112 + */ + inputTokenMint: PublicKey; + /** + * @description The mint of the token that the user wants to swap into + * @example EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + */ + outputTokenMint: PublicKey; + /** + * @description The router account that referral fees will be sent to + * @example 11111111111111111111111111111111 + */ + router: PublicKey; + /** + * @description The specified token amount for the swap + */ + specifiedTokenAmount: SpecifiedTokenAmount; + /** + * @description The user wallet account + * @example 11111111111111111111111111111111 + */ + userWallet: PublicKey; +}; + +export type QuoteResponse = { + chainId: ChainId; + expirationTime: Date; + inputToken: TokenAmountSvm; + outputToken: TokenAmountSvm; + transaction: Transaction; +};