From 5d9d3b44099cb3c2c0c2893348fe6727fe747615 Mon Sep 17 00:00:00 2001 From: Matias Poblete Date: Wed, 19 Jun 2024 18:38:13 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8Add=20horizon=20swaps=20handling=20in?= =?UTF-8?q?=20useSwapCallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../horizon/createHorizonTransaction.ts | 88 ++++++++++ src/helpers/horizon/getHorizonPath.ts | 9 +- src/hooks/useSwapCallback.tsx | 157 ++++++++++-------- 3 files changed, 182 insertions(+), 72 deletions(-) create mode 100644 src/helpers/horizon/createHorizonTransaction.ts diff --git a/src/helpers/horizon/createHorizonTransaction.ts b/src/helpers/horizon/createHorizonTransaction.ts new file mode 100644 index 00000000..ba074da6 --- /dev/null +++ b/src/helpers/horizon/createHorizonTransaction.ts @@ -0,0 +1,88 @@ +import { SorobanContextType, useSorobanReact } from "@soroban-react/core"; +import { InterfaceTrade, TradeType } from "state/routing/types"; +import {Asset, TransactionBuilder, Operation, BASE_FEE} from "@stellar/stellar-sdk"; +import { getAmount } from "./getHorizonPath"; + +export const createStellarPathPayment = async (trade: InterfaceTrade, sorobanContext: SorobanContextType) => { + const {address, activeConnector, serverHorizon, activeChain} = sorobanContext; + console.log(trade) + if(trade.tradeType == TradeType.EXACT_INPUT){ + const amount = getAmount(trade.inputAmount?.value!); + const sourceAsset = new Asset(trade.inputAmount?.currency.code!, trade.inputAmount?.currency.issuer) + const destinationAsset = new Asset(trade.outputAmount?.currency.code!, trade.outputAmount?.currency.issuer) + const account = await serverHorizon?.loadAccount(address!); + const path = trade.path?.map((asset) => { + const assetParts = asset.split(":") + if(assetParts.length == 1 && assetParts[0] == "native"){ + return Asset.native() + } + return new Asset(assetParts[0], assetParts[1]) + }) + if(!account){ + throw new Error("Account not found") + } + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: activeChain?.networkPassphrase + }).addOperation(Operation.pathPaymentStrictSend({ + sendAsset: sourceAsset, + sendAmount: amount!, + destination: address!, + destAsset: destinationAsset, + destMin: amount!, + path: path + })).setTimeout(180).build(); + const transactionXDR = transaction.toXDR(); + const signedTransaction = await activeConnector?.signTransaction(transactionXDR, { + networkPassphrase: activeChain?.networkPassphrase, + }); + if(!signedTransaction){ + throw new Error("Couldn't sign transaction"); + } + const transactionToSubmit = TransactionBuilder.fromXDR( + signedTransaction!, + activeChain?.networkPassphrase ?? '', + ); + const transactionResult = await serverHorizon?.submitTransaction(transactionToSubmit); + return transactionResult; + } + if(trade.tradeType == TradeType.EXACT_OUTPUT){ + const amount = getAmount(trade.outputAmount?.value!); + const sourceAsset = new Asset(trade.inputAmount?.currency.code!, trade.inputAmount?.currency.issuer) + const destinationAsset = new Asset(trade.outputAmount?.currency.code!, trade.outputAmount?.currency.issuer) + const account = await serverHorizon?.loadAccount(address!); + const path = trade.path?.map((asset) => { + const assetParts = asset.split(":") + if(assetParts.length == 1 && assetParts[0] == "native"){ + return Asset.native() + } + return new Asset(assetParts[0], assetParts[1]) + }) + if(!account){ + throw new Error("Account not found") + } + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: activeChain?.networkPassphrase + }).addOperation(Operation.pathPaymentStrictReceive({ + sendAsset: sourceAsset, + sendMax: amount!, + destination: address!, + destAsset: destinationAsset, + destAmount: amount!, + })).setTimeout(180).build(); + const transactionXDR = transaction.toXDR(); + const signedTransaction = await activeConnector?.signTransaction(transactionXDR, { + networkPassphrase: activeChain?.networkPassphrase, + }); + if(!signedTransaction){ + throw new Error("Couldn't sign transaction"); + } + const transactionToSubmit = TransactionBuilder.fromXDR( + signedTransaction!, + activeChain?.networkPassphrase ?? '', + ); + const transactionResult = await serverHorizon?.submitTransaction(transactionToSubmit); + return transactionResult; + } +} \ No newline at end of file diff --git a/src/helpers/horizon/getHorizonPath.ts b/src/helpers/horizon/getHorizonPath.ts index ebcb3b75..43c0a9f4 100644 --- a/src/helpers/horizon/getHorizonPath.ts +++ b/src/helpers/horizon/getHorizonPath.ts @@ -20,7 +20,8 @@ const getClassicAsset = (currency: TokenType) => { return asset } -const getAmount = (amount: string) => { +export const getAmount = (amount: string) => { + if (!amount) return; return new BigNumber(amount).dividedBy(10000000).toString() } @@ -112,7 +113,7 @@ export function getHorizonBestPath( try { const send = serverHorizon?.strictSendPaths( args.assetFrom!, - args?.amount, + args?.amount!, [args.assetTo!] ).call().then((res) => { return res.records; @@ -125,6 +126,7 @@ export function getHorizonBestPath( return maxObj; } }); + console.log(maxObj) return parseHorizonResult(maxObj, payload.tradeType); }); } catch (error) { @@ -137,7 +139,7 @@ export function getHorizonBestPath( const receive = serverHorizon?.strictReceivePaths( [args.assetFrom!], args.assetTo!, - args?.amount, + args?.amount!, ).call().then((res) => { return res.records; }); @@ -150,6 +152,7 @@ export function getHorizonBestPath( return minObj; } }); + console.log(minObj) return parseHorizonResult(minObj, payload.tradeType); }); } catch (error) { diff --git a/src/hooks/useSwapCallback.tsx b/src/hooks/useSwapCallback.tsx index 3ee72642..fef77b45 100644 --- a/src/hooks/useSwapCallback.tsx +++ b/src/hooks/useSwapCallback.tsx @@ -1,5 +1,5 @@ import { TxResponse } from '@soroban-react/contracts'; -import { SorobanContextType, useSorobanReact } from '@soroban-react/core'; +import { useSorobanReact } from '@soroban-react/core'; import BigNumber from 'bignumber.js'; import { DEFAULT_SLIPPAGE_INPUT_VALUE } from 'components/Settings/MaxSlippageSettings'; import { AppContext, SnackbarIconType } from 'contexts'; @@ -9,11 +9,12 @@ import { scValToJs } from 'helpers/convert'; import { formatTokenAmount } from 'helpers/format'; import { bigNumberToI128, bigNumberToU64 } from 'helpers/utils'; import { useContext } from 'react'; -import { InterfaceTrade, TradeType } from 'state/routing/types'; +import { InterfaceTrade, PlatformType, TradeType } from 'state/routing/types'; import { useUserSlippageToleranceWithDefault } from 'state/user/hooks'; import * as StellarSdk from '@stellar/stellar-sdk'; import { useSWRConfig } from 'swr'; import { RouterMethod, useRouterCallback } from './useRouterCallback'; +import { createStellarPathPayment } from 'helpers/horizon/createHorizonTransaction'; @@ -97,84 +98,102 @@ export function useSwapCallback( ) { const { SnackbarContext } = useContext(AppContext); const sorobanContext = useSorobanReact(); - const { activeChain, address } = sorobanContext; + const { activeChain, address, activeConnector } = sorobanContext; const routerCallback = useRouterCallback(); const allowedSlippage = useUserSlippageToleranceWithDefault(DEFAULT_SLIPPAGE_INPUT_VALUE); const { mutate } = useSWRConfig(); const doSwap = async ( simulation?: boolean, - ): Promise => { + ): Promise => { if (!trade) throw new Error('missing trade'); if (!address || !activeChain) throw new Error('wallet must be connected to swap'); if (!trade.tradeType) throw new Error('tradeType must be defined'); - const { amount0, amount1, routerMethod } = getSwapAmounts({ - tradeType: trade.tradeType, - inputAmount: trade.inputAmount?.value as string, - outputAmount: trade.outputAmount?.value as string, - allowedSlippage: allowedSlippage, - }); - const amount0ScVal = bigNumberToI128(amount0); - const amount1ScVal = bigNumberToI128(amount1); - - // fn swap_exact_tokens_for_tokens( - // e: Env, - // amount_in: i128, - // amount_out_min: i128, - // path: Vec
, - // to: Address, - // deadline: u64, - // ) -> Vec; - - // fn swap_tokens_for_exact_tokens( - // e: Env, - // amount_out: i128, - // amount_in_max: i128, - // path: Vec
, - // to: Address, - // deadline: u64, - // ) -> Vec; - - const path = trade.path?.map((address) => new StellarSdk.Address(address)); - - const pathScVal = StellarSdk.nativeToScVal(path); - - const args = [ - amount0ScVal, - amount1ScVal, - pathScVal, // path - new StellarSdk.Address(address!).toScVal(), - bigNumberToU64(BigNumber(getCurrentTimePlusOneHour())), - ]; - - try { - const result = (await routerCallback( - routerMethod, - args, - !simulation, - )) as StellarSdk.SorobanRpc.Api.GetTransactionResponse; - - //if it is a simulation should return the result - if (simulation) return result; - - if (result.status !== StellarSdk.SorobanRpc.Api.GetTransactionStatus.SUCCESS) throw result; - - const switchValues: string[] = scValToJs(result.returnValue!); - - const currencyA = switchValues?.[0]; - const currencyB = switchValues?.[switchValues?.length - 1]; - - const notificationMessage = `${formatTokenAmount(currencyA ?? '0')} ${trade?.inputAmount - ?.currency.code} for ${formatTokenAmount(currencyB ?? '0')} ${trade?.outputAmount - ?.currency.code}`; - - sendNotification(notificationMessage, 'Swapped', SnackbarIconType.SWAP, SnackbarContext); - - return { ...result, switchValues }; - } catch (error) { - throw error; + switch (trade.platform) { + case PlatformType.SOROBAN: + const { amount0, amount1, routerMethod } = getSwapAmounts({ + tradeType: trade.tradeType, + inputAmount: trade.inputAmount?.value as string, + outputAmount: trade.outputAmount?.value as string, + allowedSlippage: allowedSlippage, + }); + const amount0ScVal = bigNumberToI128(amount0); + const amount1ScVal = bigNumberToI128(amount1); + + // fn swap_exact_tokens_for_tokens( + // e: Env, + // amount_in: i128, + // amount_out_min: i128, + // path: Vec
, + // to: Address, + // deadline: u64, + // ) -> Vec; + + // fn swap_tokens_for_exact_tokens( + // e: Env, + // amount_out: i128, + // amount_in_max: i128, + // path: Vec
, + // to: Address, + // deadline: u64, + // ) -> Vec; + + const path = trade.path?.map((address) => new StellarSdk.Address(address)); + + const pathScVal = StellarSdk.nativeToScVal(path); + + const args = [ + amount0ScVal, + amount1ScVal, + pathScVal, // path + new StellarSdk.Address(address!).toScVal(), + bigNumberToU64(BigNumber(getCurrentTimePlusOneHour())), + ]; + + try { + const result = (await routerCallback( + routerMethod, + args, + !simulation, + )) as StellarSdk.SorobanRpc.Api.GetTransactionResponse; + + //if it is a simulation should return the result + if (simulation) return result; + + if (result.status !== StellarSdk.SorobanRpc.Api.GetTransactionStatus.SUCCESS) throw result; + + const switchValues: string[] = scValToJs(result.returnValue!); + + const currencyA = switchValues?.[0]; + const currencyB = switchValues?.[switchValues?.length - 1]; + + const notificationMessage = `${formatTokenAmount(currencyA ?? '0')} ${trade?.inputAmount + ?.currency.code} for ${formatTokenAmount(currencyB ?? '0')} ${trade?.outputAmount + ?.currency.code}`; + + sendNotification(notificationMessage, 'Swapped', SnackbarIconType.SWAP, SnackbarContext); + + return { ...result, switchValues }; + } catch (error) { + throw error; + } + case PlatformType.STELLAR_CLASSIC: + try { + const result = await createStellarPathPayment(trade, sorobanContext); + const notificationMessage = `${formatTokenAmount(trade.inputAmount?.value ?? '0')} ${trade?.inputAmount + ?.currency.code} for ${formatTokenAmount(trade.outputAmount?.value ?? '0')} ${trade?.outputAmount + ?.currency.code}`; + sendNotification(notificationMessage, 'Swapped', SnackbarIconType.SWAP, SnackbarContext); + return result!; + } catch (error) { + console.error(error); + } + default: + throw new Error('Unsupported platform'); } + + }; return { doSwap, isLoading: trade?.isLoading };