Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ declare module '@env' {
export const DOSH_WHITELIST: string;
export const GIT_COMMIT_HASH: string;
export const MIXPANEL_PROJECT_TOKEN: string;
export const MORALIS_API_KEY: string;
export const WALLET_CONNECT_V2_PROJECT_ID: string;
export const ZENLEDGER_CLIENT_ID: string;
export const ZENLEDGER_CLIENT_SECRET: string;
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@
"lodash.uniqby": "4.7.0",
"mixpanel-react-native": "3.3.0",
"moment": "2.29.1",
"moralis": "2.25.2",
"papaparse": "5.3.2",
"path-browserify": "1.0.1",
"react": "19.1.1",
Expand Down
3 changes: 0 additions & 3 deletions src/store/app/app.effects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,6 @@ jest.mock('../location', () => ({
jest.mock('../wallet-connect-v2/wallet-connect-v2.effects', () => ({
walletConnectV2Init: jest.fn(() => ({type: 'WALLET_CONNECT_INIT'})),
}));
jest.mock('../moralis/moralis.effects', () => ({
moralisInit: jest.fn(() => ({type: 'MORALIS_INIT'})),
}));
jest.mock('../external-services/external-services.effects', () => ({
prefetchExternalServicesData: jest.fn(() => ({
type: 'PREFETCH_EXTERNAL_SERVICES',
Expand Down
2 changes: 0 additions & 2 deletions src/store/app/app.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ import {prefetchExternalServicesData} from '../external-services/external-servic
import {receiveCrypto, sendCrypto} from '../wallet/effects/send/send';
import moment from 'moment';
import {FeedbackRateType} from '../../navigation/tabs/settings/about/screens/SendFeedback';
import {moralisInit} from '../moralis/moralis.effects';
import {walletConnectV2Init} from '../wallet-connect-v2/wallet-connect-v2.effects';
import {InAppNotificationMessages} from '../../components/modal/in-app-notification/InAppNotification';
import axios from 'axios';
Expand Down Expand Up @@ -291,7 +290,6 @@ export const startAppInit = (): Effect => async (dispatch, getState) => {
if (getState().WALLET_CONNECT_V2?.sessions?.length > 0) {
dispatch(walletConnectV2Init());
}
dispatch(moralisInit());

// Update Coinbase
dispatch(coinbaseInitialize());
Expand Down
319 changes: 319 additions & 0 deletions src/store/moralis/moralis.effects.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
import configureTestStore from '@test/store';
import {
getDecodedTransactionsByHash,
getERC20TokenAllowance,
getERC20TokenBalanceByWallet,
getERC20TokenPrice,
getMultipleEvmTokenPrices,
getMultipleSolanaTokenPrices,
getMultipleTokenPrices,
getSVMTokenBalanceByWallet,
} from './moralis.effects';
import {logManager} from '../../managers/LogManager';

jest.mock('../../managers/LogManager', () => ({
logManager: {
info: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
}));

const buildMockWallet = () => ({
id: 'wallet-1',
moralisGetTransactionVerbose: jest.fn(),
moralisGetTokenPrice: jest.fn(),
moralisGetMultipleERC20TokenPrices: jest.fn(),
moralisGetMultipleSolTokenPrices: jest.fn(),
moralisGetSolWalletPortfolio: jest.fn(),
moralisGetWalletTokenBalances: jest.fn(),
moralisGetTokenAllowance: jest.fn(),
});

const buildStoreWithWallet = (wallet: ReturnType<typeof buildMockWallet>) =>
configureTestStore({
WALLET: {
keys: {
key1: {isReadOnly: false, wallets: [wallet]},
},
customTokenOptionsByAddress: {},
},
} as any);

describe('moralis.effects (BWS proxy, wallet-signed)', () => {
let wallet: ReturnType<typeof buildMockWallet>;
let store: ReturnType<typeof configureTestStore>;

beforeEach(() => {
wallet = buildMockWallet();
store = buildStoreWithWallet(wallet);
});

describe('getERC20TokenPrice', () => {
it('calls moralisGetTokenPrice with the hex chain', async () => {
const raw = {usdPrice: 123};
wallet.moralisGetTokenPrice.mockResolvedValueOnce(raw);

const result = await store.dispatch(
getERC20TokenPrice({address: '0xabc', chain: 'eth'}),
);

expect(wallet.moralisGetTokenPrice).toHaveBeenCalledWith({
address: '0xabc',
chain: '0x1',
});
expect(result).toEqual(raw);
});
});

describe('getDecodedTransactionsByHash', () => {
it('calls moralisGetTransactionVerbose', async () => {
const raw = {logs: [], to_address_label: 'Contract'};
wallet.moralisGetTransactionVerbose.mockResolvedValueOnce(raw);

const result = await store.dispatch(
getDecodedTransactionsByHash({
transactionHash: '0xhash',
chain: 'matic',
}),
);

expect(wallet.moralisGetTransactionVerbose).toHaveBeenCalledWith({
transactionHash: '0xhash',
chain: '0x89',
});
expect(result).toEqual(raw);
});
});

describe('getMultipleEvmTokenPrices', () => {
it('sends tokens as [{tokenAddress}] (Moralis SDK input shape)', async () => {
wallet.moralisGetMultipleERC20TokenPrices.mockResolvedValueOnce([]);

await getMultipleEvmTokenPrices(wallet as any, ['0xaaa', '0xbbb'], '0x1');

expect(wallet.moralisGetMultipleERC20TokenPrices).toHaveBeenCalledWith({
chain: '0x1',
include: 'percent_change',
tokens: [{tokenAddress: '0xaaa'}, {tokenAddress: '0xbbb'}],
});
});
});

describe('getMultipleSolanaTokenPrices', () => {
it('calls moralisGetMultipleSolTokenPrices', async () => {
wallet.moralisGetMultipleSolTokenPrices.mockResolvedValueOnce([]);

await getMultipleSolanaTokenPrices(wallet as any, ['solAddr1']);

expect(wallet.moralisGetMultipleSolTokenPrices).toHaveBeenCalledWith({
addresses: ['solAddr1'],
network: 'mainnet',
});
});
});

describe('getMultipleTokenPrices', () => {
it('maps EVM prices to the unified shape', async () => {
wallet.moralisGetMultipleERC20TokenPrices.mockResolvedValueOnce([
{tokenAddress: '0xaaa', usdPrice: 2, usdPrice24hrPercentChange: -1.5},
]);

const result = await store.dispatch(
getMultipleTokenPrices({addresses: ['0xaaa'], chain: 'eth'}),
);

expect(result).toEqual([
{tokenAddress: '0xaaa', usdPrice: 2, '24hrPercentChange': -1.5},
]);
});

it('uses the SOL prices endpoint for SVM chains', async () => {
wallet.moralisGetMultipleSolTokenPrices.mockResolvedValueOnce([
{tokenAddress: 'solAddr', usdPrice: 3, usdPrice24hrPercentChange: 1},
]);

const result = await store.dispatch(
getMultipleTokenPrices({addresses: ['solAddr'], chain: 'sol'}),
);

expect(wallet.moralisGetMultipleSolTokenPrices).toHaveBeenCalledWith({
addresses: ['solAddr'],
network: 'mainnet',
});
expect(result).toEqual([
{tokenAddress: 'solAddr', usdPrice: 3, '24hrPercentChange': 1},
]);
});
});

describe('getERC20TokenBalanceByWallet', () => {
it('returns [] without calling BWS for unsupported chains', async () => {
const result = await store.dispatch(
getERC20TokenBalanceByWallet({address: '0xabc', chain: 'btc'}),
);

expect(result).toEqual([]);
expect(wallet.moralisGetWalletTokenBalances).not.toHaveBeenCalled();
});

it('calls moralisGetWalletTokenBalances', async () => {
const raw = [{token_address: '0xaaa', balance: '1'}];
wallet.moralisGetWalletTokenBalances.mockResolvedValueOnce(raw);

const result = await store.dispatch(
getERC20TokenBalanceByWallet({address: '0xabc', chain: 'arb'}),
);

expect(wallet.moralisGetWalletTokenBalances).toHaveBeenCalledWith({
address: '0xabc',
chain: '0xa4b1',
});
expect(result).toEqual(raw);
});
});

describe('getSVMTokenBalanceByWallet', () => {
it('returns the tokens from the moralisGetSolWalletPortfolio response', async () => {
const tokens = [{mint: 'solAddr', amount: '1'}];
wallet.moralisGetSolWalletPortfolio.mockResolvedValueOnce({tokens});

const result = await store.dispatch(
getSVMTokenBalanceByWallet({
address: 'solWallet',
chain: 'sol',
network: 'mainnet',
}),
);

expect(wallet.moralisGetSolWalletPortfolio).toHaveBeenCalledWith({
address: 'solWallet',
network: 'mainnet',
});
expect(result).toEqual(tokens);
});

it('throws for non-SVM chains without calling BWS', async () => {
await expect(
store.dispatch(
getSVMTokenBalanceByWallet({
address: '0xabc',
chain: 'eth',
network: 'mainnet',
}),
),
).rejects.toThrow('Unsupported chain for SVM token balance');
expect(wallet.moralisGetSolWalletPortfolio).not.toHaveBeenCalled();
});
});

describe('getERC20TokenAllowance', () => {
it('sends owner address and hex chain to moralisGetTokenAllowance', async () => {
const raw = {result: []};
wallet.moralisGetTokenAllowance.mockResolvedValueOnce(raw);

const result = await store.dispatch(
getERC20TokenAllowance({chain: 'op', ownerAddress: '0xowner'}),
);

expect(wallet.moralisGetTokenAllowance).toHaveBeenCalledWith({
ownerAddress: '0xowner',
chain: '0xa',
});
expect(result).toEqual(raw);
});

it('includes limit and cursor when provided', async () => {
wallet.moralisGetTokenAllowance.mockResolvedValueOnce({result: []});

await store.dispatch(
getERC20TokenAllowance({
chain: 'eth',
ownerAddress: '0xowner',
limit: 50,
cursor: 'abc',
}),
);

expect(wallet.moralisGetTokenAllowance).toHaveBeenCalledWith({
ownerAddress: '0xowner',
chain: '0x1',
limit: 50,
cursor: 'abc',
});
});
});

describe('getSigningWallet resolution (no wallets yet, e.g. fresh install)', () => {
const emptyStore = () =>
configureTestStore({
WALLET: {keys: {}, customTokenOptionsByAddress: {}},
} as any);

beforeEach(() => {
(logManager.error as jest.Mock).mockClear();
});

it('getMultipleTokenPrices returns [] without logging an error', async () => {
const result = await emptyStore().dispatch(
getMultipleTokenPrices({addresses: ['0xaaa'], chain: 'eth'}),
);

expect(result).toEqual([]);
expect(logManager.error).not.toHaveBeenCalled();
});

it('getSVMTokenBalanceByWallet returns [] without logging an error', async () => {
const result = await emptyStore().dispatch(
getSVMTokenBalanceByWallet({
address: 'solWallet',
chain: 'sol',
network: 'mainnet',
}),
);

expect(result).toEqual([]);
expect(logManager.error).not.toHaveBeenCalled();
});

it('getERC20TokenBalanceByWallet returns [] without logging an error', async () => {
const result = await emptyStore().dispatch(
getERC20TokenBalanceByWallet({address: '0xabc', chain: 'eth'}),
);

expect(result).toEqual([]);
expect(logManager.error).not.toHaveBeenCalled();
});

it('getERC20TokenPrice rejects without logging an error', async () => {
await expect(
emptyStore().dispatch(
getERC20TokenPrice({address: '0xabc', chain: 'eth'}),
),
).rejects.toThrow('No wallet available to sign the Moralis request');
expect(logManager.error).not.toHaveBeenCalled();
});

it('getDecodedTransactionsByHash rejects without logging an error', async () => {
await expect(
emptyStore().dispatch(
getDecodedTransactionsByHash({
transactionHash: '0xhash',
chain: 'eth',
}),
),
).rejects.toThrow('No wallet available to sign the Moralis request');
expect(logManager.error).not.toHaveBeenCalled();
});

it('getERC20TokenAllowance rejects without logging an error', async () => {
await expect(
emptyStore().dispatch(
getERC20TokenAllowance({chain: 'eth', ownerAddress: '0xowner'}),
),
).rejects.toThrow('No wallet available to sign the Moralis request');
expect(logManager.error).not.toHaveBeenCalled();
});
});
});
Loading
Loading