Skip to content

Commit

Permalink
erge branch 'develop' of https://github.com/rainbow-me/rainbow into @…
Browse files Browse the repository at this point in the history
…benisgold/crosschain-swaps-rap
  • Loading branch information
benisgold committed Nov 25, 2024
2 parents 84ca209 + f8808d8 commit 0f37430
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 77 deletions.
29 changes: 5 additions & 24 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,16 @@ import { designSystemPlaygroundEnabled, reactNativeDisableYellowBox, showNetwork
import monitorNetwork from '@/debugging/network';
import { Playground } from '@/design-system/playground/Playground';
import RainbowContextWrapper from '@/helpers/RainbowContext';
import * as keychain from '@/model/keychain';
import { Navigation } from '@/navigation';
import { PersistQueryClientProvider, persistOptions, queryClient } from '@/react-query';
import store, { AppDispatch, type AppState } from '@/redux/store';
import { MainThemeProvider, useTheme } from '@/theme/ThemeContext';
import { addressKey } from '@/utils/keychainConstants';
import { MainThemeProvider } from '@/theme/ThemeContext';
import { SharedValuesProvider } from '@/helpers/SharedValuesContext';
import { InitialRouteContext } from '@/navigation/initialRoute';
import { Portal } from '@/react-native-cool-modals/Portal';
import { NotificationsHandler } from '@/notifications/NotificationsHandler';
import { analyticsV2 } from '@/analytics';
import { getOrCreateDeviceId, securelyHashWalletAddress } from '@/analytics/utils';
import { getOrCreateDeviceId } from '@/analytics/utils';
import { logger, RainbowError } from '@/logger';
import * as ls from '@/storage';
import { migrate } from '@/migrations';
Expand All @@ -38,7 +36,6 @@ import { ReviewPromptAction } from '@/storage/schema';
import { initializeRemoteConfig } from '@/model/remoteConfig';
import { NavigationContainerRef } from '@react-navigation/native';
import { RootStackParamList } from '@/navigation/types';
import { Address } from 'viem';
import { IS_ANDROID, IS_DEV } from '@/env';
import { prefetchDefaultFavorites } from '@/resources/favorites';
import Routes from '@/navigation/Routes';
Expand Down Expand Up @@ -102,27 +99,11 @@ function Root() {

const isReturningUser = ls.device.get(['isReturningUser']);
const [deviceId, deviceIdWasJustCreated] = await getOrCreateDeviceId();
const currentWalletAddress = await keychain.loadString(addressKey);
const currentWalletAddressHash =
typeof currentWalletAddress === 'string' ? securelyHashWalletAddress(currentWalletAddress as Address) : undefined;

Sentry.setUser({
id: deviceId,
currentWalletAddress: currentWalletAddressHash,
});

/**
* Add helpful values to `analyticsV2` instance
*/
// Initial telemetry; amended with wallet context later in `useInitializeWallet`
Sentry.setUser({ id: deviceId });
analyticsV2.setDeviceId(deviceId);
if (currentWalletAddressHash) {
analyticsV2.setCurrentWalletAddressHash(currentWalletAddressHash);
}

/**
* `analyticsv2` has all it needs to function.
*/
analyticsV2.identify({});
analyticsV2.identify();

const isReviewInitialized = ls.review.get(['initialized']);
if (!isReviewInitialized) {
Expand Down
2 changes: 1 addition & 1 deletion src/analytics/__mocks__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const analyticsV2 = {
screen: jest.fn(),
track: jest.fn(),
setDeviceId: jest.fn(),
setCurrentWalletAddressHash: jest.fn(),
setWalletContext: jest.fn(),
enable: jest.fn(),
disable: jest.fn(),
event,
Expand Down
6 changes: 3 additions & 3 deletions src/analytics/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe.skip('@/analytics', () => {
test('track', () => {
const analytics = new Analytics();

analytics.setCurrentWalletAddressHash('hash');
analytics.setWalletContext({ walletAddressHash: 'hash', walletType: 'owned' });
analytics.track(analytics.event.pressedButton);

expect(analytics.client.track).toHaveBeenCalledWith(analytics.event.pressedButton, {
Expand All @@ -29,7 +29,7 @@ describe.skip('@/analytics', () => {
test('identify', () => {
const analytics = new Analytics();

analytics.setCurrentWalletAddressHash('hash');
analytics.setWalletContext({ walletAddressHash: 'hash', walletType: 'owned' });
analytics.setDeviceId('id');
analytics.identify({ currency: 'USD' });

Expand All @@ -42,7 +42,7 @@ describe.skip('@/analytics', () => {
test('screen', () => {
const analytics = new Analytics();

analytics.setCurrentWalletAddressHash('hash');
analytics.setWalletContext({ walletAddressHash: 'hash', walletType: 'owned' });
analytics.screen(Routes.BACKUP_SHEET);

expect(analytics.client.screen).toHaveBeenCalledWith(Routes.BACKUP_SHEET, {
Expand Down
2 changes: 2 additions & 0 deletions src/analytics/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ jest.mock('@/model/keychain', () => ({
loadString: jest.fn(),
}));

jest.mock('@/redux/store');

jest.mock('@sentry/react-native', () => ({
setUser: jest.fn(),
}));
Expand Down
43 changes: 26 additions & 17 deletions src/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { EventProperties, event } from '@/analytics/event';
import { UserProperties } from '@/analytics/userProperties';
import { logger, RainbowError } from '@/logger';
import { device } from '@/storage';
import { WalletContext } from './utils';

const isTesting = IS_TESTING === 'true';

export class Analytics {
client: any;
currentWalletAddressHash?: string;
client: typeof rudderClient;
deviceId?: string;
walletAddressHash?: WalletContext['walletAddressHash'];
walletType?: WalletContext['walletType'];
event = event;
disabled: boolean;

Expand All @@ -30,38 +32,44 @@ export class Analytics {
* here. This uses the `deviceId` as the identifier, and attaches the hashed
* wallet address as a property, if available.
*/
identify(userProperties: UserProperties) {
identify(userProperties?: UserProperties) {
if (this.disabled) return;
const metadata = this.getDefaultMetadata();
this.client.identify(this.deviceId, {
...userProperties,
...metadata,
});
this.client.identify(
this.deviceId as string,
{
...metadata,
...userProperties,
},
{}
);
}

/**
* Sends a `screen` event.
*/
screen(routeName: string, params: Record<string, any> = {}): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
screen(routeName: string, params: Record<string, any> = {}, walletContext?: WalletContext): void {
if (this.disabled) return;
const metadata = this.getDefaultMetadata();
this.client.screen(routeName, { ...params, ...metadata });
this.client.screen(routeName, { ...metadata, ...walletContext, ...params });
}

/**
* Sends an event. Param `event` must exist in
* `@/analytics/event`, and if properties are associated with it, they must
* be defined as part of `EventProperties` in the same file
*/
track<T extends keyof EventProperties>(event: T, params?: EventProperties[T]) {
track<T extends keyof EventProperties>(event: T, params?: EventProperties[T], walletContext?: WalletContext) {
if (this.disabled) return;
const metadata = this.getDefaultMetadata();
this.client.track(event, { ...params, ...metadata });
this.client.track(event, { ...metadata, ...walletContext, ...params });
}

private getDefaultMetadata() {
return {
walletAddressHash: this.currentWalletAddressHash,
walletAddressHash: this.walletAddressHash,
walletType: this.walletType,
};
}

Expand All @@ -80,17 +88,18 @@ export class Analytics {
* `identify()`, you must do that on your own.
*/
setDeviceId(deviceId: string) {
logger.debug(`[Analytics]: Set deviceId on analytics instance`);
this.deviceId = deviceId;
logger.debug(`[Analytics]: Set deviceId on analytics instance`);
}

/**
* Set `currentWalletAddressHash` for use in events. This DOES NOT call
* Set `walletAddressHash` and `walletType` for use in events. This DOES NOT call
* `identify()`, you must do that on your own.
*/
setCurrentWalletAddressHash(currentWalletAddressHash: string) {
logger.debug(`[Analytics]: Set currentWalletAddressHash on analytics instance`);
this.currentWalletAddressHash = currentWalletAddressHash;
setWalletContext(walletContext: WalletContext) {
this.walletAddressHash = walletContext.walletAddressHash;
this.walletType = walletContext.walletType;
logger.debug(`[Analytics]: Set walletAddressHash on analytics instance`);
}

/**
Expand Down
36 changes: 35 additions & 1 deletion src/analytics/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { nanoid } from 'nanoid/non-secure';
import { SECURE_WALLET_HASH_KEY } from 'react-native-dotenv';
import type { Address } from 'viem';

import * as ls from '@/storage';
import * as keychain from '@/model/keychain';
import { analyticsUserIdentifier } from '@/utils/keychainConstants';
import { logger, RainbowError } from '@/logger';
import { computeHmac, SupportedAlgorithm } from '@ethersproject/sha2';
import { findWalletWithAccount } from '@/helpers/findWalletWithAccount';
import store from '@/redux/store';
import { EthereumWalletType } from '@/helpers/walletTypes';

/**
* Returns the device id in a type-safe manner. It will throw if no device ID
Expand Down Expand Up @@ -58,7 +62,7 @@ export async function getOrCreateDeviceId(): Promise<[string, boolean]> {
}
}

export function securelyHashWalletAddress(walletAddress: `0x${string}`): string | undefined {
function securelyHashWalletAddress(walletAddress: Address): string | undefined {
if (!SECURE_WALLET_HASH_KEY) {
logger.error(new RainbowError(`[securelyHashWalletAddress]: Required .env variable SECURE_WALLET_HASH_KEY does not exist`));
}
Expand All @@ -80,3 +84,33 @@ export function securelyHashWalletAddress(walletAddress: `0x${string}`): string
logger.error(new RainbowError(`[securelyHashWalletAddress]: Wallet address hashing failed`));
}
}

export type WalletContext = {
walletType?: 'owned' | 'hardware' | 'watched';
walletAddressHash?: string;
};

export async function getWalletContext(address: Address): Promise<WalletContext> {
// currentAddressStore address is initialized to ''
if (!address || address === ('' as Address)) return {};

// walletType maybe undefined after initial wallet creation
const { wallets } = store.getState();
const wallet = findWalletWithAccount(wallets.wallets || {}, address);

const walletType = (
{
[EthereumWalletType.mnemonic]: 'owned',
[EthereumWalletType.privateKey]: 'owned',
[EthereumWalletType.seed]: 'owned',
[EthereumWalletType.readOnly]: 'watched',
[EthereumWalletType.bluetooth]: 'hardware',
} as const
)[wallet?.type!];
const walletAddressHash = securelyHashWalletAddress(address);

return {
walletType,
walletAddressHash,
};
}
79 changes: 52 additions & 27 deletions src/handlers/tokenSearch.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { isAddress } from '@ethersproject/address';
import { qs } from 'url-parse';
import { RainbowFetchClient } from '../rainbow-fetch';
import { TokenSearchThreshold, TokenSearchTokenListId, TokenSearchUniswapAssetKey } from '@/entities';
import { TokenSearchThreshold, TokenSearchTokenListId } from '@/entities';
import { logger, RainbowError } from '@/logger';
import { EthereumAddress } from '@rainbow-me/swaps';
import { RainbowToken, TokenSearchToken } from '@/entities/tokens';
import { chainsName } from '@/chains';
import { ChainId } from '@/chains/types';

type TokenSearchApiResponse = {
data: TokenSearchToken[];
};
const ALL_VERIFIED_TOKENS_PARAM = '/?list=verifiedAssets';

const tokenSearchApi = new RainbowFetchClient({
const tokenSearchHttp = new RainbowFetchClient({
baseURL: 'https://token-search.rainbow.me/v2',
headers: {
'Accept': 'application/json',
Expand All @@ -20,50 +18,77 @@ const tokenSearchApi = new RainbowFetchClient({
timeout: 30000,
});

function parseTokenSearch(assets: TokenSearchToken[]): RainbowToken[] {
return assets.map(token => {
const networkKeys = Object.keys(token.networks);
const chainId = Number(networkKeys[0]);
const network = chainsName[chainId];
return {
...token,
chainId,
address: token.networks['1']?.address || token.networks[chainId]?.address,
network,
mainnet_address: token.networks['1']?.address,
};
});
}

export const tokenSearch = async (searchParams: {
chainId: number;
chainId: ChainId;
fromChainId?: number | '';
keys: TokenSearchUniswapAssetKey[];
keys: (keyof RainbowToken)[];
list: TokenSearchTokenListId;
threshold: TokenSearchThreshold;
query: string;
}): Promise<RainbowToken[]> => {
const queryParams: {
keys: TokenSearchUniswapAssetKey[];
keys: string;
list: TokenSearchTokenListId;
threshold: TokenSearchThreshold;
query?: string;
fromChainId?: number;
} = {
keys: searchParams.keys,
keys: searchParams.keys.join(','),
list: searchParams.list,
threshold: searchParams.threshold,
query: searchParams.query,
};

const { chainId, query } = searchParams;

const isAddressSearch = query && isAddress(query);

if (isAddressSearch) {
queryParams.keys = `networks.${chainId}.address`;
}

const url = `/?${qs.stringify(queryParams)}`;
const isSearchingVerifiedAssets = queryParams.list === 'verifiedAssets';

try {
if (isAddress(searchParams.query)) {
// @ts-ignore
params.keys = `networks.${searchParams.chainId}.address`;
const tokenSearch = await tokenSearchHttp.get<{ data: TokenSearchToken[] }>(url);

if (isAddressSearch && isSearchingVerifiedAssets) {
if (tokenSearch && tokenSearch.data.data.length > 0) {
return parseTokenSearch(tokenSearch.data.data);
}

const allVerifiedTokens = await tokenSearchHttp.get<{ data: TokenSearchToken[] }>(ALL_VERIFIED_TOKENS_PARAM);

const addressQuery = query.trim().toLowerCase();

const addressMatchesOnOtherChains = allVerifiedTokens.data.data.filter(a =>
Object.values(a.networks).some(n => n?.address === addressQuery)
);

return parseTokenSearch(addressMatchesOnOtherChains);
}
const url = `/?${qs.stringify(queryParams)}`;
const tokenSearch = await tokenSearchApi.get<TokenSearchApiResponse>(url);

if (!tokenSearch.data?.data) {
return [];
}

return tokenSearch.data.data.map(token => {
const networkKeys = Object.keys(token.networks);
const chainId = Number(networkKeys[0]);
const network = chainsName[chainId];
return {
...token,
chainId,
address: token.networks['1']?.address || token.networks[chainId]?.address,
network,
mainnet_address: token.networks['1']?.address,
};
});
return parseTokenSearch(tokenSearch.data.data);
} catch (e: any) {
logger.error(new RainbowError(`[tokenSearch]: An error occurred while searching for query`), {
query: searchParams.query,
Expand Down
Loading

0 comments on commit 0f37430

Please sign in to comment.