UNPKG

@reown/appkit-controllers

Version:

#### 🔗 [Website](https://reown.com/appkit)

658 lines • 28.4 kB
import { proxy, subscribe as sub } from 'valtio/vanilla'; import { subscribeKey as subKey } from 'valtio/vanilla/utils'; import { NumberUtil } from '@reown/appkit-common'; import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common'; import { W3mFrameRpcConstants } from '@reown/appkit-wallet/utils'; import { BalanceUtil } from '../utils/BalanceUtil.js'; import { getActiveNetworkTokenAddress } from '../utils/ChainControllerUtil.js'; import { ConstantsUtil } from '../utils/ConstantsUtil.js'; import { CoreHelperUtil } from '../utils/CoreHelperUtil.js'; import { SwapApiUtil } from '../utils/SwapApiUtil.js'; import { SwapCalculationUtil } from '../utils/SwapCalculationUtil.js'; import { withErrorBoundary } from '../utils/withErrorBoundary.js'; import { AccountController } from './AccountController.js'; import { AlertController } from './AlertController.js'; import { BlockchainApiController } from './BlockchainApiController.js'; import { ChainController } from './ChainController.js'; import { ConnectionController } from './ConnectionController.js'; import { ConnectorController } from './ConnectorController.js'; import { EventsController } from './EventsController.js'; import { RouterController } from './RouterController.js'; import { SnackController } from './SnackController.js'; // -- Constants ---------------------------------------- // export const INITIAL_GAS_LIMIT = 150000; export const TO_AMOUNT_DECIMALS = 6; class TransactionError extends Error { constructor(message, shortMessage) { super(message); this.name = 'TransactionError'; this.shortMessage = shortMessage; } } // -- State --------------------------------------------- // const initialState = { // Loading states initializing: false, initialized: false, loadingPrices: false, loadingQuote: false, loadingApprovalTransaction: false, loadingBuildTransaction: false, loadingTransaction: false, // Error states fetchError: false, // Approval & Swap transaction states approvalTransaction: undefined, swapTransaction: undefined, transactionError: undefined, // Input values sourceToken: undefined, sourceTokenAmount: '', sourceTokenPriceInUSD: 0, toToken: undefined, toTokenAmount: '', toTokenPriceInUSD: 0, networkPrice: '0', networkBalanceInUSD: '0', networkTokenSymbol: '', inputError: undefined, // Request values slippage: ConstantsUtil.CONVERT_SLIPPAGE_TOLERANCE, // Tokens tokens: undefined, popularTokens: undefined, suggestedTokens: undefined, foundTokens: undefined, myTokensWithBalance: undefined, tokensPriceMap: {}, // Calculations gasFee: '0', gasPriceInUSD: 0, priceImpact: undefined, maxSlippage: undefined, providerFee: undefined }; const state = proxy(initialState); // -- Controller ---------------------------------------- // const controller = { state, subscribe(callback) { return sub(state, () => callback(state)); }, subscribeKey(key, callback) { return subKey(state, key, callback); }, getParams() { const caipAddress = ChainController.state.activeCaipAddress; const namespace = ChainController.state.activeChain; const address = CoreHelperUtil.getPlainAddress(caipAddress); const networkAddress = getActiveNetworkTokenAddress(); const connectorId = ConnectorController.getConnectorId(namespace); if (!address) { throw new Error('No address found to swap the tokens from.'); } const invalidToToken = !state.toToken?.address || !state.toToken?.decimals; const invalidSourceToken = !state.sourceToken?.address || !state.sourceToken?.decimals || !NumberUtil.bigNumber(state.sourceTokenAmount).gt(0); const invalidSourceTokenAmount = !state.sourceTokenAmount; return { networkAddress, fromAddress: address, fromCaipAddress: caipAddress, sourceTokenAddress: state.sourceToken?.address, toTokenAddress: state.toToken?.address, toTokenAmount: state.toTokenAmount, toTokenDecimals: state.toToken?.decimals, sourceTokenAmount: state.sourceTokenAmount, sourceTokenDecimals: state.sourceToken?.decimals, invalidToToken, invalidSourceToken, invalidSourceTokenAmount, availableToSwap: caipAddress && !invalidToToken && !invalidSourceToken && !invalidSourceTokenAmount, isAuthConnector: connectorId === CommonConstantsUtil.CONNECTOR_ID.AUTH }; }, setSourceToken(sourceToken) { if (!sourceToken) { state.sourceToken = sourceToken; state.sourceTokenAmount = ''; state.sourceTokenPriceInUSD = 0; return; } state.sourceToken = sourceToken; SwapController.setTokenPrice(sourceToken.address, 'sourceToken'); }, setSourceTokenAmount(amount) { state.sourceTokenAmount = amount; }, setToToken(toToken) { if (!toToken) { state.toToken = toToken; state.toTokenAmount = ''; state.toTokenPriceInUSD = 0; return; } state.toToken = toToken; SwapController.setTokenPrice(toToken.address, 'toToken'); }, setToTokenAmount(amount) { state.toTokenAmount = amount ? NumberUtil.formatNumberToLocalString(amount, TO_AMOUNT_DECIMALS) : ''; }, async setTokenPrice(address, target) { let price = state.tokensPriceMap[address] || 0; if (!price) { state.loadingPrices = true; price = await SwapController.getAddressPrice(address); } if (target === 'sourceToken') { state.sourceTokenPriceInUSD = price; } else if (target === 'toToken') { state.toTokenPriceInUSD = price; } if (state.loadingPrices) { state.loadingPrices = false; } if (SwapController.getParams().availableToSwap) { SwapController.swapTokens(); } }, switchTokens() { if (state.initializing || !state.initialized) { return; } const newSourceToken = state.toToken ? { ...state.toToken } : undefined; const newToToken = state.sourceToken ? { ...state.sourceToken } : undefined; const newSourceTokenAmount = newSourceToken && state.toTokenAmount === '' ? '1' : state.toTokenAmount; SwapController.setSourceToken(newSourceToken); SwapController.setToToken(newToToken); SwapController.setSourceTokenAmount(newSourceTokenAmount); SwapController.setToTokenAmount(''); SwapController.swapTokens(); }, resetState() { state.myTokensWithBalance = initialState.myTokensWithBalance; state.tokensPriceMap = initialState.tokensPriceMap; state.initialized = initialState.initialized; state.sourceToken = initialState.sourceToken; state.sourceTokenAmount = initialState.sourceTokenAmount; state.sourceTokenPriceInUSD = initialState.sourceTokenPriceInUSD; state.toToken = initialState.toToken; state.toTokenAmount = initialState.toTokenAmount; state.toTokenPriceInUSD = initialState.toTokenPriceInUSD; state.networkPrice = initialState.networkPrice; state.networkTokenSymbol = initialState.networkTokenSymbol; state.networkBalanceInUSD = initialState.networkBalanceInUSD; state.inputError = initialState.inputError; state.myTokensWithBalance = initialState.myTokensWithBalance; }, resetValues() { const { networkAddress } = SwapController.getParams(); const networkToken = state.tokens?.find(token => token.address === networkAddress); SwapController.setSourceToken(networkToken); SwapController.setToToken(undefined); }, getApprovalLoadingState() { return state.loadingApprovalTransaction; }, clearError() { state.transactionError = undefined; }, async initializeState() { if (state.initializing) { return; } state.initializing = true; if (!state.initialized) { try { await SwapController.fetchTokens(); state.initialized = true; } catch (error) { state.initialized = false; SnackController.showError('Failed to initialize swap'); RouterController.goBack(); } } state.initializing = false; }, async fetchTokens() { const { networkAddress } = SwapController.getParams(); await SwapController.getTokenList(); await SwapController.getNetworkTokenPrice(); await SwapController.getMyTokensWithBalance(); const networkToken = state.tokens?.find(token => token.address === networkAddress); if (networkToken) { state.networkTokenSymbol = networkToken.symbol; SwapController.setSourceToken(networkToken); SwapController.setSourceTokenAmount('1'); } }, async getTokenList() { const tokens = await SwapApiUtil.getTokenList(); state.tokens = tokens; state.popularTokens = tokens.sort((aTokenInfo, bTokenInfo) => { if (aTokenInfo.symbol < bTokenInfo.symbol) { return -1; } if (aTokenInfo.symbol > bTokenInfo.symbol) { return 1; } return 0; }); state.suggestedTokens = tokens.filter(token => { if (ConstantsUtil.SWAP_SUGGESTED_TOKENS.includes(token.symbol)) { return true; } return false; }, {}); }, async getAddressPrice(address) { const existPrice = state.tokensPriceMap[address]; if (existPrice) { return existPrice; } const response = await BlockchainApiController.fetchTokenPrice({ addresses: [address] }); const fungibles = response?.fungibles || []; const allTokens = [...(state.tokens || []), ...(state.myTokensWithBalance || [])]; const symbol = allTokens?.find(token => token.address === address)?.symbol; const price = fungibles.find(p => p.symbol.toLowerCase() === symbol?.toLowerCase())?.price || 0; const priceAsFloat = parseFloat(price.toString()); state.tokensPriceMap[address] = priceAsFloat; return priceAsFloat; }, async getNetworkTokenPrice() { const { networkAddress } = SwapController.getParams(); const response = await BlockchainApiController.fetchTokenPrice({ addresses: [networkAddress] }).catch(() => { SnackController.showError('Failed to fetch network token price'); return { fungibles: [] }; }); const token = response.fungibles?.[0]; const price = token?.price.toString() || '0'; state.tokensPriceMap[networkAddress] = parseFloat(price); state.networkTokenSymbol = token?.symbol || ''; state.networkPrice = price; }, async getMyTokensWithBalance(forceUpdate) { const balances = await BalanceUtil.getMyTokensWithBalance(forceUpdate); const swapBalances = SwapApiUtil.mapBalancesToSwapTokens(balances); if (!swapBalances) { return; } await SwapController.getInitialGasPrice(); SwapController.setBalances(swapBalances); }, setBalances(balances) { const { networkAddress } = SwapController.getParams(); const caipNetwork = ChainController.state.activeCaipNetwork; if (!caipNetwork) { return; } const networkToken = balances.find(token => token.address === networkAddress); balances.forEach(token => { state.tokensPriceMap[token.address] = token.price || 0; }); state.myTokensWithBalance = balances.filter(token => token.address.startsWith(caipNetwork.caipNetworkId)); state.networkBalanceInUSD = networkToken ? NumberUtil.multiply(networkToken.quantity.numeric, networkToken.price).toString() : '0'; }, async getInitialGasPrice() { const res = await SwapApiUtil.fetchGasPrice(); if (!res) { return { gasPrice: null, gasPriceInUSD: null }; } switch (ChainController.state?.activeCaipNetwork?.chainNamespace) { case 'solana': state.gasFee = res.standard ?? '0'; state.gasPriceInUSD = NumberUtil.multiply(res.standard, state.networkPrice) .div(1e9) .toNumber(); return { gasPrice: BigInt(state.gasFee), gasPriceInUSD: Number(state.gasPriceInUSD) }; case 'eip155': default: // eslint-disable-next-line no-case-declarations const value = res.standard ?? '0'; // eslint-disable-next-line no-case-declarations const gasFee = BigInt(value); // eslint-disable-next-line no-case-declarations const gasLimit = BigInt(INITIAL_GAS_LIMIT); // eslint-disable-next-line no-case-declarations const gasPrice = SwapCalculationUtil.getGasPriceInUSD(state.networkPrice, gasLimit, gasFee); state.gasFee = value; state.gasPriceInUSD = gasPrice; return { gasPrice: gasFee, gasPriceInUSD: gasPrice }; } }, // -- Swap -------------------------------------- // async swapTokens() { const address = AccountController.state.address; const sourceToken = state.sourceToken; const toToken = state.toToken; const haveSourceTokenAmount = NumberUtil.bigNumber(state.sourceTokenAmount).gt(0); if (!haveSourceTokenAmount) { SwapController.setToTokenAmount(''); } if (!toToken || !sourceToken || state.loadingPrices || !haveSourceTokenAmount) { return; } state.loadingQuote = true; const amountDecimal = NumberUtil.bigNumber(state.sourceTokenAmount) .times(10 ** sourceToken.decimals) .round(0); try { const quoteResponse = await BlockchainApiController.fetchSwapQuote({ userAddress: address, from: sourceToken.address, to: toToken.address, gasPrice: state.gasFee, amount: amountDecimal.toString() }); state.loadingQuote = false; const quoteToAmount = quoteResponse?.quotes?.[0]?.toAmount; if (!quoteToAmount) { AlertController.open({ shortMessage: 'Incorrect amount', longMessage: 'Please enter a valid amount' }, 'error'); return; } const toTokenAmount = NumberUtil.bigNumber(quoteToAmount) .div(10 ** toToken.decimals) .toString(); SwapController.setToTokenAmount(toTokenAmount); const isInsufficientToken = SwapController.hasInsufficientToken(state.sourceTokenAmount, sourceToken.address); if (isInsufficientToken) { state.inputError = 'Insufficient balance'; } else { state.inputError = undefined; SwapController.setTransactionDetails(); } } catch (error) { state.loadingQuote = false; state.inputError = 'Insufficient balance'; } }, // -- Create Transactions -------------------------------------- // async getTransaction() { const { fromCaipAddress, availableToSwap } = SwapController.getParams(); const sourceToken = state.sourceToken; const toToken = state.toToken; if (!fromCaipAddress || !availableToSwap || !sourceToken || !toToken || state.loadingQuote) { return undefined; } try { state.loadingBuildTransaction = true; const hasAllowance = await SwapApiUtil.fetchSwapAllowance({ userAddress: fromCaipAddress, tokenAddress: sourceToken.address, sourceTokenAmount: state.sourceTokenAmount, sourceTokenDecimals: sourceToken.decimals }); let transaction = undefined; if (hasAllowance) { transaction = await SwapController.createSwapTransaction(); } else { transaction = await SwapController.createAllowanceTransaction(); } state.loadingBuildTransaction = false; state.fetchError = false; return transaction; } catch (error) { RouterController.goBack(); SnackController.showError('Failed to check allowance'); state.loadingBuildTransaction = false; state.approvalTransaction = undefined; state.swapTransaction = undefined; state.fetchError = true; return undefined; } }, async createAllowanceTransaction() { const { fromCaipAddress, sourceTokenAddress, toTokenAddress } = SwapController.getParams(); if (!fromCaipAddress || !toTokenAddress) { return undefined; } if (!sourceTokenAddress) { throw new Error('createAllowanceTransaction - No source token address found.'); } try { const response = await BlockchainApiController.generateApproveCalldata({ from: sourceTokenAddress, to: toTokenAddress, userAddress: fromCaipAddress }); const transaction = { data: response.tx.data, to: CoreHelperUtil.getPlainAddress(response.tx.from), gasPrice: BigInt(response.tx.eip155.gasPrice), value: BigInt(response.tx.value), toAmount: state.toTokenAmount }; state.swapTransaction = undefined; state.approvalTransaction = { data: transaction.data, to: transaction.to, gasPrice: transaction.gasPrice, value: transaction.value, toAmount: transaction.toAmount }; return { data: transaction.data, to: transaction.to, gasPrice: transaction.gasPrice, value: transaction.value, toAmount: transaction.toAmount }; } catch (error) { RouterController.goBack(); SnackController.showError('Failed to create approval transaction'); state.approvalTransaction = undefined; state.swapTransaction = undefined; state.fetchError = true; return undefined; } }, async createSwapTransaction() { const { networkAddress, fromCaipAddress, sourceTokenAmount } = SwapController.getParams(); const sourceToken = state.sourceToken; const toToken = state.toToken; if (!fromCaipAddress || !sourceTokenAmount || !sourceToken || !toToken) { return undefined; } const amount = ConnectionController.parseUnits(sourceTokenAmount, sourceToken.decimals)?.toString(); try { const response = await BlockchainApiController.generateSwapCalldata({ userAddress: fromCaipAddress, from: sourceToken.address, to: toToken.address, amount: amount, disableEstimate: true }); const isSourceTokenIsNetworkToken = sourceToken.address === networkAddress; const gas = BigInt(response.tx.eip155.gas); const gasPrice = BigInt(response.tx.eip155.gasPrice); const transaction = { data: response.tx.data, to: CoreHelperUtil.getPlainAddress(response.tx.to), gas, gasPrice, value: isSourceTokenIsNetworkToken ? BigInt(amount ?? '0') : BigInt('0'), toAmount: state.toTokenAmount }; state.gasPriceInUSD = SwapCalculationUtil.getGasPriceInUSD(state.networkPrice, gas, gasPrice); state.approvalTransaction = undefined; state.swapTransaction = transaction; return transaction; } catch (error) { RouterController.goBack(); SnackController.showError('Failed to create transaction'); state.approvalTransaction = undefined; state.swapTransaction = undefined; state.fetchError = true; return undefined; } }, // -- Send Transactions --------------------------------- // async sendTransactionForApproval(data) { const { fromAddress, isAuthConnector } = SwapController.getParams(); state.loadingApprovalTransaction = true; const approveLimitMessage = `Approve limit increase in your wallet`; if (isAuthConnector) { RouterController.pushTransactionStack({ onSuccess() { SnackController.showLoading(approveLimitMessage); } }); } else { SnackController.showLoading(approveLimitMessage); } try { await ConnectionController.sendTransaction({ address: fromAddress, to: data.to, data: data.data, value: data.value, chainNamespace: 'eip155' }); await SwapController.swapTokens(); await SwapController.getTransaction(); state.approvalTransaction = undefined; state.loadingApprovalTransaction = false; } catch (err) { const error = err; state.transactionError = error?.shortMessage; state.loadingApprovalTransaction = false; SnackController.showError(error?.shortMessage || 'Transaction error'); EventsController.sendEvent({ type: 'track', event: 'SWAP_APPROVAL_ERROR', properties: { message: error?.shortMessage || error?.message || 'Unknown', network: ChainController.state.activeCaipNetwork?.caipNetworkId || '', swapFromToken: SwapController.state.sourceToken?.symbol || '', swapToToken: SwapController.state.toToken?.symbol || '', swapFromAmount: SwapController.state.sourceTokenAmount || '', swapToAmount: SwapController.state.toTokenAmount || '', isSmartAccount: AccountController.state.preferredAccountTypes?.eip155 === W3mFrameRpcConstants.ACCOUNT_TYPES.SMART_ACCOUNT } }); } }, async sendTransactionForSwap(data) { if (!data) { return undefined; } const { fromAddress, toTokenAmount, isAuthConnector } = SwapController.getParams(); state.loadingTransaction = true; const snackbarPendingMessage = `Swapping ${state.sourceToken?.symbol} to ${NumberUtil.formatNumberToLocalString(toTokenAmount, 3)} ${state.toToken?.symbol}`; const snackbarSuccessMessage = `Swapped ${state.sourceToken?.symbol} to ${NumberUtil.formatNumberToLocalString(toTokenAmount, 3)} ${state.toToken?.symbol}`; if (isAuthConnector) { RouterController.pushTransactionStack({ onSuccess() { RouterController.replace('Account'); SnackController.showLoading(snackbarPendingMessage); controller.resetState(); } }); } else { SnackController.showLoading('Confirm transaction in your wallet'); } try { const forceUpdateAddresses = [state.sourceToken?.address, state.toToken?.address].join(','); const transactionHash = await ConnectionController.sendTransaction({ address: fromAddress, to: data.to, data: data.data, value: data.value, chainNamespace: 'eip155' }); state.loadingTransaction = false; SnackController.showSuccess(snackbarSuccessMessage); EventsController.sendEvent({ type: 'track', event: 'SWAP_SUCCESS', properties: { network: ChainController.state.activeCaipNetwork?.caipNetworkId || '', swapFromToken: SwapController.state.sourceToken?.symbol || '', swapToToken: SwapController.state.toToken?.symbol || '', swapFromAmount: SwapController.state.sourceTokenAmount || '', swapToAmount: SwapController.state.toTokenAmount || '', isSmartAccount: AccountController.state.preferredAccountTypes?.eip155 === W3mFrameRpcConstants.ACCOUNT_TYPES.SMART_ACCOUNT } }); controller.resetState(); if (!isAuthConnector) { RouterController.replace('Account'); } controller.getMyTokensWithBalance(forceUpdateAddresses); return transactionHash; } catch (err) { const error = err; state.transactionError = error?.shortMessage; state.loadingTransaction = false; SnackController.showError(error?.shortMessage || 'Transaction error'); EventsController.sendEvent({ type: 'track', event: 'SWAP_ERROR', properties: { message: error?.shortMessage || error?.message || 'Unknown', network: ChainController.state.activeCaipNetwork?.caipNetworkId || '', swapFromToken: SwapController.state.sourceToken?.symbol || '', swapToToken: SwapController.state.toToken?.symbol || '', swapFromAmount: SwapController.state.sourceTokenAmount || '', swapToAmount: SwapController.state.toTokenAmount || '', isSmartAccount: AccountController.state.preferredAccountTypes?.eip155 === W3mFrameRpcConstants.ACCOUNT_TYPES.SMART_ACCOUNT } }); return undefined; } }, // -- Checks -------------------------------------------- // hasInsufficientToken(sourceTokenAmount, sourceTokenAddress) { const isInsufficientSourceTokenForSwap = SwapCalculationUtil.isInsufficientSourceTokenForSwap(sourceTokenAmount, sourceTokenAddress, state.myTokensWithBalance); return isInsufficientSourceTokenForSwap; }, // -- Calculations -------------------------------------- // setTransactionDetails() { const { toTokenAddress, toTokenDecimals } = SwapController.getParams(); if (!toTokenAddress || !toTokenDecimals) { return; } state.gasPriceInUSD = SwapCalculationUtil.getGasPriceInUSD(state.networkPrice, BigInt(state.gasFee), BigInt(INITIAL_GAS_LIMIT)); state.priceImpact = SwapCalculationUtil.getPriceImpact({ sourceTokenAmount: state.sourceTokenAmount, sourceTokenPriceInUSD: state.sourceTokenPriceInUSD, toTokenPriceInUSD: state.toTokenPriceInUSD, toTokenAmount: state.toTokenAmount }); state.maxSlippage = SwapCalculationUtil.getMaxSlippage(state.slippage, state.toTokenAmount); state.providerFee = SwapCalculationUtil.getProviderFee(state.sourceTokenAmount); } }; // Export the controller wrapped with our error boundary export const SwapController = withErrorBoundary(controller); //# sourceMappingURL=SwapController.js.map