@reown/appkit-controllers
Version:
#### 🔗 [Website](https://reown.com/appkit)
658 lines • 28.4 kB
JavaScript
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