From 4e6dc4c4efe337975144c5111d70c8fe6de267b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Brzezin=CC=81ski?= Date: Wed, 3 Jul 2024 23:27:00 +0200 Subject: [PATCH] wallet swap --- package.json | 3 +- utils/transactions.ts | 163 +++++++++++++++++++++++++++++++++++++++++- yarn.lock | 22 ++++-- 3 files changed, 179 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index ec6b765..444c93a 100644 --- a/package.json +++ b/package.json @@ -38,10 +38,11 @@ "@blockworks-foundation/mango-feeds": "0.1.7", "@blockworks-foundation/mango-v4": "0.31.3", "@blockworks-foundation/mango-v4-settings": "0.14.24", + "@blockworks-foundation/mangolana": "0.0.17", "@glitchful-dev/sol-apy-sdk": "3.0.2", "@headlessui/react": "1.6.6", "@heroicons/react": "2.0.10", - "@project-serum/anchor": "0.25.0", + "@project-serum/anchor": "0.26.0", "@pythnetwork/client": "2.15.0", "@raydium-io/raydium-sdk": "1.3.1-beta.57", "@solana/spl-governance": "0.3.27", diff --git a/utils/transactions.ts b/utils/transactions.ts index f707880..4b9dba9 100644 --- a/utils/transactions.ts +++ b/utils/transactions.ts @@ -3,8 +3,10 @@ import { Bank, FlashLoanType, Group, + MANGO_ROUTER_API_URL, MangoAccount, MangoClient, + MangoError, MangoSignatureStatus, RouteInfo, TokenIndex, @@ -15,6 +17,7 @@ import { toNative, toNativeI80F48, toUiDecimals, + tryStringify, } from '@blockworks-foundation/mango-v4' import { TOKEN_PROGRAM_ID } from '@solana/spl-governance' import { @@ -28,16 +31,22 @@ import { Connection, Keypair, PublicKey, + RpcResponseAndContext, SYSVAR_INSTRUCTIONS_PUBKEY, + SignatureResult, SystemProgram, + Transaction, TransactionInstruction, TransactionMessage, VersionedTransaction, } from '@solana/web3.js' import { floorToDecimal } from './numbers' -import { BOOST_ACCOUNT_PREFIX } from './constants' +import { BOOST_ACCOUNT_PREFIX, JUPITER_V6_QUOTE_API_MAINNET } from './constants' import { notify } from './notifications' import { getStakableTokensDataForMint } from './tokens' +import { JupiterV6RouteInfo } from 'types/jupiter' +import { WalletContextState } from '@solana/wallet-adapter-react' +import { awaitTransactionSignatureConfirmation } from '@blockworks-foundation/mangolana/lib/transactions' export const withdrawAndClose = async ( client: MangoClient, @@ -962,3 +971,155 @@ export const getNextAccountNumber = (accounts: MangoAccount[]): number => { } return 0 } + +export const walletSwap = async ( + selectedRoute: JupiterV6RouteInfo, + connection: Connection, + slippage: number, + wallet: WalletContextState, + client: MangoClient, +) => { + const vtx = await fetchJupiterWalletSwapTransaction( + selectedRoute, + wallet!.publicKey!, + slippage, + selectedRoute.origin, + ) + + const latestBlockhash = await connection.getLatestBlockhash() + const sign = wallet.signTransaction! + const signed = await sign(vtx) + + const txid = await sendTxAndConfirm( + client.opts.multipleConnections, + connection, + signed, + latestBlockhash, + ) + return { txid } +} + +/** Given a Jupiter route, fetch the transaction for the user to sign. + **This function should ONLY be used for wallet swaps* */ +export const fetchJupiterWalletSwapTransaction = async ( + selectedRoute: JupiterV6RouteInfo, + userPublicKey: PublicKey, + slippage: number, + origin?: 'mango' | 'jupiter' | 'raydium', +): Promise => { + // docs https://station.jup.ag/api-v6/post-swap + const params: { + quoteResponse: JupiterV6RouteInfo + userPublicKey: PublicKey + slippageBps: number + autoCreateOutAta?: boolean + wrapAndUnwrapSol?: boolean + } = { + // response from /quote api + quoteResponse: selectedRoute, + // user public key to be used for the swap + userPublicKey, + slippageBps: Math.ceil(slippage * 100), + } + + if (origin === 'mango') { + params.autoCreateOutAta = true + params.wrapAndUnwrapSol = true + } + + const transactions = await ( + await fetch( + `${ + origin === 'mango' ? MANGO_ROUTER_API_URL : JUPITER_V6_QUOTE_API_MAINNET + }/swap`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }, + ) + ).json() + + const { swapTransaction } = transactions + const parsedSwapTransaction = VersionedTransaction.deserialize( + Buffer.from(swapTransaction, 'base64'), + ) + return parsedSwapTransaction +} + +export const sendTxAndConfirm = async ( + multipleConnections: Connection[] = [], + connection: Connection, + tx: Transaction | VersionedTransaction, + latestBlockhash: { + lastValidBlockHeight: number + blockhash: string + }, +) => { + let signature = '' + const abortController = new AbortController() + try { + const allConnections = [connection, ...multipleConnections] + const rawTransaction = tx.serialize() + signature = await Promise.any( + allConnections.map((c) => { + return c.sendRawTransaction(rawTransaction, { + skipPreflight: true, + }) + }), + ) + await Promise.any( + allConnections.map((c) => + awaitTransactionSignatureConfirmation({ + txid: signature, + confirmLevel: 'processed', + connection: c, + timeoutStrategy: { + block: latestBlockhash, + }, + abortSignal: abortController.signal, + }), + ), + ) + abortController.abort() + return signature + } catch (e) { + abortController.abort() + if (e instanceof AggregateError) { + for (const individualError of e.errors) { + const stringifiedError = tryStringify(individualError) + throw new MangoError({ + txid: signature, + message: `${ + stringifiedError + ? stringifiedError + : individualError + ? individualError + : 'Unknown error' + }`, + }) + } + } + if (isErrorWithSignatureResult(e)) { + const stringifiedError = tryStringify(e?.value?.err) + throw new MangoError({ + txid: signature, + message: `${stringifiedError ? stringifiedError : e?.value?.err}`, + }) + } + const stringifiedError = tryStringify(e) + throw new MangoError({ + txid: signature, + message: `${stringifiedError ? stringifiedError : e}`, + }) + } +} + +function isErrorWithSignatureResult( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + err: any, +): err is RpcResponseAndContext { + return err && typeof err.value !== 'undefined' +} diff --git a/yarn.lock b/yarn.lock index e68d093..25dd98b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -201,6 +201,14 @@ superstruct "^0.15.4" toml "^3.0.0" +"@coral-xyz/borsh@^0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.26.0.tgz#d054f64536d824634969e74138f9f7c52bbbc0d5" + integrity sha512-uCZ0xus0CszQPHYfWAqKS5swS1UxvePu83oOF+TWpUkedsNlg6p2p4azxZNSSqwXb9uXMFgxhuMBX9r3Xoi0vQ== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + "@coral-xyz/borsh@^0.27.0": version "0.27.0" resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.27.0.tgz#700c647ea5262b1488957ac7fb4e8acf72c72b63" @@ -1375,18 +1383,18 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== -"@project-serum/anchor@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.25.0.tgz#88ee4843336005cf5a64c80636ce626f0996f503" - integrity sha512-E6A5Y/ijqpfMJ5psJvbw0kVTzLZFUcOFgs6eSM2M2iWE1lVRF18T6hWZVNl6zqZsoz98jgnNHtVGJMs+ds9A7A== +"@project-serum/anchor@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.26.0.tgz#99e15a3923a5d10514f8185b2d3909e5699d60d5" + integrity sha512-Nq+COIjE1135T7qfnOHEn7E0q39bQTgXLFk837/rgFe6Hkew9WML7eHsS+lSYD2p3OJaTiUOHTAq1lHy36oIqQ== dependencies: - "@project-serum/borsh" "^0.2.5" - "@solana/web3.js" "^1.36.0" + "@coral-xyz/borsh" "^0.26.0" + "@solana/web3.js" "^1.68.0" base64-js "^1.5.1" bn.js "^5.1.2" bs58 "^4.0.1" buffer-layout "^1.2.2" - camelcase "^5.3.1" + camelcase "^6.3.0" cross-fetch "^3.1.5" crypto-hash "^1.3.0" eventemitter3 "^4.0.7"