@reown/appkit-controllers
Version:
The full stack toolkit to build onchain app UX.
264 lines • 12.8 kB
JavaScript
import { parseUnits } from 'viem';
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { ConstantsUtil } from '@reown/appkit-common';
import { BlockchainApiController, ChainController, ConnectionController, ConnectorController, RouterController, SwapController } from '../../exports/index.js';
import { SwapApiUtil } from '../../src/utils/SwapApiUtil.js';
import { allowanceResponse, balanceResponse, gasPriceResponse, networkTokenPriceResponse, swapCalldataResponse, swapQuoteResponse, tokensResponse } from '../mocks/SwapController.js';
// - Mocks ---------------------------------------------------------------------
const caipNetwork = {
id: 137,
caipNetworkId: 'eip155:137',
name: 'Polygon',
chainNamespace: ConstantsUtil.CHAIN.EVM,
nativeCurrency: {
name: 'Polygon',
decimals: 18,
symbol: 'MATIC'
},
rpcUrls: {
default: {
http: ['']
}
}
};
const arbitrumNetwork = {
id: 42161,
caipNetworkId: 'eip155:42161',
name: 'Arbitrum One',
chainNamespace: ConstantsUtil.CHAIN.EVM,
nativeCurrency: {
name: 'Ether',
decimals: 18,
symbol: 'ETH'
},
rpcUrls: {
default: {
http: ['']
}
}
};
const chain = ConstantsUtil.CHAIN.EVM;
const caipAddress = 'eip155:1:0x123';
// MATIC
const networkTokenAddress = 'eip155:137:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
// AVAX
const toTokenAddress = 'eip155:137:0x2c89bbc92bd86f8075d1decc58c7f4e0107f286b';
const sourceTokenAmount = '1';
// - Setup ---------------------------------------------------------------------
beforeAll(async () => {
const mockAdapter = {
namespace: ConstantsUtil.CHAIN.EVM,
caipNetworks: [caipNetwork]
};
ChainController.initialize([mockAdapter], [caipNetwork], {
connectionControllerClient: vi.fn()
});
ChainController.setActiveCaipNetwork(caipNetwork);
ChainController.setAccountProp('caipAddress', caipAddress, chain);
ChainController.setAccountProp('address', '0x123', chain);
vi.spyOn(BlockchainApiController, 'fetchSwapTokens').mockResolvedValue(tokensResponse);
vi.spyOn(BlockchainApiController, 'getBalance').mockResolvedValue(balanceResponse);
vi.spyOn(BlockchainApiController, 'fetchSwapQuote').mockResolvedValue(swapQuoteResponse);
vi.spyOn(BlockchainApiController, 'fetchTokenPrice').mockResolvedValue(networkTokenPriceResponse);
vi.spyOn(BlockchainApiController, 'generateSwapCalldata').mockResolvedValue(swapCalldataResponse);
vi.spyOn(BlockchainApiController, 'fetchSwapAllowance').mockResolvedValue(allowanceResponse);
vi.spyOn(SwapApiUtil, 'fetchGasPrice').mockResolvedValue(gasPriceResponse);
vi.spyOn(ConnectionController, 'parseUnits').mockResolvedValue(parseUnits('1', 18));
await SwapController.initializeState();
await SwapController.getTokenList();
const toToken = SwapController.state.myTokensWithBalance?.[1];
SwapController.setToToken(toToken);
});
// -- Tests --------------------------------------------------------------------
describe('SwapController', () => {
it('should set sourceToken as expected', () => {
expect(SwapController.state.sourceToken?.address).toEqual(networkTokenAddress);
});
it('should set toToken as expected', () => {
expect(SwapController.state.toToken?.address).toEqual(toTokenAddress);
expect(SwapController.state.toTokenPriceInUSD).toEqual(40.101925674);
});
it('should calculate swap values as expected', async () => {
SwapController.setSourceTokenAmount(sourceTokenAmount);
await SwapController.swapTokens();
expect(SwapController.state.gasPriceInUSD).toEqual(0.00648630001383744);
expect(SwapController.state.priceImpact).toEqual(3.952736601951709);
expect(SwapController.state.maxSlippage).toEqual(0.0001726);
});
it('should handle large token amounts with 18 decimal token', async () => {
const inputAmount = '499999999999999';
const expectedAmount = '499999999999999000000000000000000';
const fetchSwapQuoteSpy = vi.spyOn(BlockchainApiController, 'fetchSwapQuote');
SwapController.setSourceTokenAmount(inputAmount);
SwapController.setSourceToken({
address: '0x123',
decimals: 18
});
await SwapController.swapTokens();
await vi.waitFor(() => expect(fetchSwapQuoteSpy).toHaveBeenCalledWith(expect.objectContaining({
amount: expectedAmount
})), { timeout: 2500 });
const capturedAmount = fetchSwapQuoteSpy.mock.calls[0]?.[0]?.amount;
expect(capturedAmount).toBe(expectedAmount);
expect(capturedAmount).not.toContain('e');
expect(capturedAmount).not.toContain('E');
expect(capturedAmount?.length).toBe(33);
});
it('should handle fetchSwapQuote error correctly', async () => {
SwapController.resetState();
const mockFetchQuote = vi.spyOn(BlockchainApiController, 'fetchSwapQuote');
mockFetchQuote.mockRejectedValueOnce(new Error('Quote error'));
const sourceToken = SwapController.state.tokens?.[0];
const toToken = SwapController.state.tokens?.[1];
SwapController.setSourceToken(sourceToken);
SwapController.setToToken(toToken);
SwapController.setSourceTokenAmount('1');
await SwapController.swapTokens();
expect(mockFetchQuote).toHaveBeenCalled();
expect(SwapController.state.loadingQuote).toBe(false);
expect(SwapController.state.inputError).toBe('Insufficient balance');
});
it('should reset values as expected', () => {
SwapController.resetValues();
expect(SwapController.state.toToken).toEqual(undefined);
expect(SwapController.state.toTokenAmount).toEqual('');
expect(SwapController.state.toTokenPriceInUSD).toEqual(0);
});
it('should clear to token amount when source token amount is cleared', async () => {
SwapController.setSourceTokenAmount('1.0');
await SwapController.swapTokens();
const toTokenAmount = Number(SwapController.state.toTokenAmount);
expect(toTokenAmount).toBeGreaterThan(0);
SwapController.setSourceTokenAmount('');
await SwapController.swapTokens();
expect(SwapController.state.toTokenAmount).toEqual('');
});
it('should replace SwapPreview view when approval transaction is approved', async () => {
const connectionControllerClientSpy = vi
.spyOn(ConnectionController, 'sendTransaction')
.mockImplementationOnce(() => Promise.resolve(null));
vi.spyOn(ConnectorController, 'getConnectorId').mockReturnValue('AUTH');
vi.spyOn(RouterController, 'pushTransactionStack').mockImplementationOnce(() => Promise.resolve());
const onEmbeddedWalletApprovalSuccessSpy = vi.spyOn(SwapController, 'onEmbeddedWalletApprovalSuccess');
SwapController.state.approvalTransaction = {
to: '0x123',
data: '0x123',
value: 10000n,
toAmount: '1'
};
await SwapController.sendTransactionForApproval(SwapController.state.approvalTransaction);
expect(connectionControllerClientSpy).toHaveBeenCalled();
expect(RouterController.pushTransactionStack).toHaveBeenCalledWith({
onSuccess: onEmbeddedWalletApprovalSuccessSpy
});
});
it('should correctly swap source and destination token addresses', async () => {
// Set up initial state with distinct tokens
const initialSourceToken = SwapController.state.myTokensWithBalance?.[0];
const initialToToken = SwapController.state.myTokensWithBalance?.[1];
SwapController.setSourceToken(initialSourceToken);
SwapController.setToToken(initialToToken);
const originalSourceAddress = SwapController.state.sourceToken?.address;
const originalToAddress = SwapController.state.toToken?.address;
await SwapController.switchTokens();
expect(SwapController.state.sourceToken?.address).toEqual(originalToAddress);
expect(SwapController.state.toToken?.address).toEqual(originalSourceAddress);
});
describe('getParams()', () => {
it('should use ChainController.getAccountData before falling back to activeCaipAddress', () => {
const mockNamespace = ConstantsUtil.CHAIN.EVM;
const mockCaipAddressFromAccount = 'eip155:1:1';
const mockActiveCaipAddress = 'eip155:1:2';
vi.spyOn(ChainController, 'state', 'get').mockReturnValue({
...ChainController.state,
activeChain: mockNamespace,
activeCaipAddress: mockActiveCaipAddress,
activeCaipNetwork: caipNetwork
});
const getCaipAddressSpy = vi.spyOn(ChainController, 'getAccountData').mockReturnValue({
caipAddress: mockCaipAddressFromAccount
});
const params = SwapController.getParams();
expect(getCaipAddressSpy).toHaveBeenCalledWith(mockNamespace);
expect(params.fromCaipAddress).toBe(mockCaipAddressFromAccount);
});
it('should fallback to activeCaipAddress when ChainController.getAccountData returns undefined', () => {
const mockNamespace = ConstantsUtil.CHAIN.EVM;
const mockActiveCaipAddress = 'eip155:1:0xFallback';
vi.spyOn(ChainController, 'state', 'get').mockReturnValue({
...ChainController.state,
activeChain: mockNamespace,
activeCaipAddress: mockActiveCaipAddress,
activeCaipNetwork: caipNetwork
});
const getCaipAddressSpy = vi
.spyOn(ChainController, 'getAccountData')
.mockReturnValue(undefined);
const params = SwapController.getParams();
expect(getCaipAddressSpy).toHaveBeenCalledWith(mockNamespace);
expect(params.fromCaipAddress).toBe(mockActiveCaipAddress);
});
it('should throw error when no address is available from either source', () => {
const mockNamespace = ConstantsUtil.CHAIN.EVM;
vi.spyOn(ChainController, 'state', 'get').mockReturnValue({
...ChainController.state,
activeChain: mockNamespace,
activeCaipAddress: undefined,
activeCaipNetwork: caipNetwork
});
vi.spyOn(ChainController, 'getAccountData').mockReturnValue(undefined);
expect(() => SwapController.getParams()).toThrow('No address found to swap the tokens from.');
});
});
it('should show chain-specific suggested token first when active network is Arbitrum', async () => {
SwapController.state.tokens = undefined;
ChainController.state.activeCaipNetwork = arbitrumNetwork;
const mockTokens = [
{
address: 'eip155:42161:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
symbol: 'ETH',
name: 'Ether',
decimals: 18,
logoUri: ''
},
{
address: 'eip155:42161:0x0000000000000000000000000000000000000000',
symbol: 'USD₮0',
name: 'Tether USD0',
decimals: 6,
logoUri: ''
}
];
vi.spyOn(BlockchainApiController, 'fetchSwapTokens').mockResolvedValueOnce({
tokens: mockTokens
});
await SwapController.getTokenList();
expect(SwapController.state.suggestedTokens?.[0]?.symbol).toBe('USD₮0');
});
it('should show chain-specific suggested token first when active network is Arbitrum', async () => {
SwapController.state.tokens = undefined;
ChainController.state.activeCaipNetwork = arbitrumNetwork;
const mockTokens = [
{
address: 'eip155:42161:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
symbol: 'ETH',
name: 'Ether',
decimals: 18,
logoUri: ''
},
{
address: 'eip155:42161:0x0000000000000000000000000000000000000000',
symbol: 'USD₮0',
name: 'Tether USD0',
decimals: 6,
logoUri: ''
}
];
vi.spyOn(BlockchainApiController, 'fetchSwapTokens').mockResolvedValueOnce({
tokens: mockTokens
});
await SwapController.getTokenList();
expect(SwapController.state.suggestedTokens?.[0]?.symbol).toBe('USD₮0');
});
});
//# sourceMappingURL=SwapController.test.js.map