UNPKG

@swapper-finance/sdk

Version:
324 lines (295 loc) 10.1 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { useState, useCallback, useRef } from "react"; import { ethers, BigNumber } from "ethers"; import { useNavigate } from "react-router-dom"; import { Chain, Route, SwapperAppPayload, TokenPrices } from "@src/models"; import { TokenWithChain } from "@src/models"; import { ProcessingState, RoutePath, HistoryEntryAction, HistoryEntryStatus, Balances, BalancesSnapshotMap, } from "@src/interfaces"; import { CASH_TOKEN_ADDRESS_BY_CHAIN } from "@src/config"; import { calculateGasCashback, weiToHumanReadable } from "@src/utils"; import { SwapMode } from "@src/interfaces"; import swapperConfig from "../../../swapper.config"; import getTokenAmountOut from "@src/utils/getTokenAmountOut"; import { UserOperationGasPrice } from "../PrivySmartWalletProvider"; export const useTransactionState = ( transactionState: ProcessingState, setTransactionState: React.Dispatch<React.SetStateAction<ProcessingState>>, smartWalletAddress: string | undefined, executeTransaction: ( calls: any[], chainId: string, gasCostInUSDC?: string, userOperationGasPrice?: UserOperationGasPrice, ) => Promise<string | undefined>, addTransactionToLocalHistory: (entry: any, address: string) => void, fetchHistory: () => void, fetchAllBalances: () => Promise<void>, route: Route | undefined, token: TokenWithChain | undefined, cashToken: TokenWithChain | undefined, swapMode: SwapMode, chain: Chain | undefined, setNewPurchaseCounter: React.Dispatch<React.SetStateAction<number>>, setBalancesSnapshots: React.Dispatch< React.SetStateAction<BalancesSnapshotMap> >, tokensBalances: Record<string, Record<string, Balances>>, tokensPrices: TokenPrices, isExternalMode: boolean, integrationConfig: Partial<SwapperAppPayload>, ) => { const navigate = useNavigate(); const [cashbackAmount, setCashbackAmount] = useState<string | undefined>( undefined, ); const [pointsAmount, setPointsAmount] = useState<string | undefined>( undefined, ); const [transactionHash, setTransactionHash] = useState(""); const [tokenOutAmount, setTokenOutAmount] = useState(""); const [transactionError, setTransactionError] = useState(""); const executionIdRef = useRef(0); const executeSwapTransaction = useCallback(async () => { const currentExecutionId = ++executionIdRef.current; if (!route || !token || !smartWalletAddress || !chain) { setTransactionError("Invalid swap transaction, please try again."); setTransactionState(ProcessingState.TransactionError); navigate(RoutePath.Processing); return; } setTransactionHash(""); setTransactionState(ProcessingState.Pending); navigate(RoutePath.Processing); try { // Prepare transaction calls const calls: { to: `0x${string}`; value?: bigint; data?: `0x${string}`; }[] = []; if (route.transactions?.approve) { calls.push({ to: route.transactions?.approve.to, value: BigInt(route.transactions?.approve.value), data: route.transactions?.approve.data, }); } if (route.transactions?.swap) { calls.push({ to: route.transactions?.swap.to, value: BigInt(route.transactions?.swap.value), data: route.transactions?.swap.data, }); } // Execute the transaction const txHash = await executeTransaction( calls, chain.chainId, route?.gasCostInUSDC, route?.userOperationGasPrice, ); if (!txHash) { throw new Error("Transaction failed"); } setTransactionHash(txHash); // Send tx to backend and process it in stats collector const processTxPayload = { transactionHash: txHash, blockchainId: chain.chainId, }; try { const response = await fetch(`${swapperConfig.apiUrl}/process-tx`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(processTxPayload), }); if (!response.ok) { addTransactionToLocalHistory( { txHash, action: swapMode === SwapMode.Buy ? HistoryEntryAction.buy : HistoryEntryAction.sell, priceUsd: "-", tokenAddress: token.address, date: new Date(), status: HistoryEntryStatus.pending, }, smartWalletAddress, ); console.error(`process-tx failed: ${response.statusText}`); } else { const { pointsForTransaction } = await response.json(); setPointsAmount(pointsForTransaction.toString()); await fetchHistory(); } } catch (error) { console.error("Error processing transaction:", error); } // Get the swap target address const swapTargetAddress = route?.transactions?.swap?.to; const provider = new ethers.providers.JsonRpcProvider( chain.publicRpcUrls[0], ); const receipt = await provider.waitForTransaction(txHash); const receivedTokenAmount = await getTokenAmountOut( receipt, swapMode === SwapMode.Buy ? token.address : CASH_TOKEN_ADDRESS_BY_CHAIN[chain.chainId], isExternalMode ? integrationConfig.externalWalletAddress! : smartWalletAddress, ); if (currentExecutionId === executionIdRef.current) { setTokenOutAmount( weiToHumanReadable({ amount: receivedTokenAmount, decimals: swapMode === SwapMode.Buy ? token.decimals : cashToken?.decimals || 6, precisionFractionalPlaces: 5, }), ); } // TODO consider moving this to backend since public rpc urls are unreliable if ( route?.gasCostInUSDC && chain?.publicRpcUrls[0] && swapTargetAddress && currentExecutionId === executionIdRef.current ) { // Get transaction receipt from hash const gasCostInUSDC = BigNumber.from(route.gasCostInUSDC); // Run gas cashback calculation and balance fetching in parallel const [gasCashbackResult] = await Promise.all([ calculateGasCashback( receipt, gasCostInUSDC, CASH_TOKEN_ADDRESS_BY_CHAIN[chain.chainId], cashToken?.decimals || 6, smartWalletAddress, [swapTargetAddress], ), fetchAllBalances(), ]); if (gasCashbackResult.cashbackAmount.gt(0)) { if (currentExecutionId === executionIdRef.current) { setCashbackAmount( weiToHumanReadable({ amount: gasCashbackResult.cashbackAmount.toString(), decimals: cashToken?.decimals || 6, precisionFractionalPlaces: 4, }), ); } } } else { await fetchAllBalances(); } if (currentExecutionId === executionIdRef.current) { setTransactionState(ProcessingState.TransactionSuccess); } setBalancesSnapshots((prev) => { const next = { ...prev }; // Snapshot for total cash if (!next.totalCash) { const totalBefore = Object.entries(tokensBalances).reduce( (sum, [chainId, balancesMap]) => { const cashAddr = CASH_TOKEN_ADDRESS_BY_CHAIN[chainId]?.toLowerCase(); if (!cashAddr) return sum; const record = balancesMap[cashAddr]; return ( sum + (record?.humanReadable ? Number(record.humanReadable) : 0) ); }, 0, ); next.totalCash = { initialAmount: totalBefore.toString(), initialValue: totalBefore.toFixed(2), }; } // Snapshot for the specific token const tokenAddress = swapMode === SwapMode.Buy ? token.address.toLowerCase() : CASH_TOKEN_ADDRESS_BY_CHAIN[chain.chainId].toLowerCase(); const key = `${chain.chainId}-${tokenAddress}`; if (!next[key]) { const balRecord = tokensBalances[chain.chainId]?.[token.address.toLowerCase()]; const amountBefore = balRecord?.humanReadable || "0"; const priceBefore = tokensPrices[chain.chainId]?.[token.address] || "0"; const valueBefore = ( Number(amountBefore) * Number(priceBefore) ).toFixed(2); next[key] = { initialAmount: amountBefore, initialValue: valueBefore, }; } return next; }); setNewPurchaseCounter((prev) => prev + 1); } catch (err) { console.error(err); if (currentExecutionId === executionIdRef.current) { let errorMessage = err instanceof Error ? err.message : String(err); if ( (err as Error)?.message?.includes( "The Provider is disconnected from all chains", ) ) { errorMessage = "User transaction signature denied."; } setTransactionError(errorMessage); setTransactionState(ProcessingState.TransactionError); } } }, [ route, token, executeTransaction, smartWalletAddress, swapMode, chain, cashToken, addTransactionToLocalHistory, fetchHistory, fetchAllBalances, navigate, setTransactionState, setNewPurchaseCounter, setBalancesSnapshots, tokensBalances, tokensPrices, integrationConfig.externalWalletAddress, isExternalMode, ]); return { transactionHash, setTransactionHash, transactionState, setTransactionState, tokenOutAmount, setTokenOutAmount, cashbackAmount, setCashbackAmount, pointsAmount, setPointsAmount, executeSwapTransaction, transactionError, }; };