Skip to content

Commit

Permalink
[BX-915] Consolidated Transaction History (#816)
Browse files Browse the repository at this point in the history
Co-authored-by: gregs <[email protected]>
  • Loading branch information
derHowie and greg-schrammel authored Aug 3, 2023
1 parent bcefbcd commit 3287f09
Show file tree
Hide file tree
Showing 19 changed files with 762 additions and 583 deletions.
2 changes: 1 addition & 1 deletion src/core/network/addys.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createHttpClient } from './internal/createHttpClient';

export const addysHttp = createHttpClient({
baseUrl: 'https://addys.p.rainbow.me/v2',
baseUrl: 'https://addys.p.rainbow.me/v3',
headers: { Authorization: `Bearer ${process.env.ADDYS_API_KEY}` as string },
});
5 changes: 1 addition & 4 deletions src/core/network/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
export { etherscanHttp } from './etherscan';
export { meteorologyHttp } from './meteorology';
export {
refractionAddressWs,
refractionAddressMessages,
} from './refractionAddressWs';
export { refractionAddressMessages } from './refractionAddressWs';
export {
refractionAssetsWs,
refractionAssetsMessages,
Expand Down
9 changes: 0 additions & 9 deletions src/core/network/refractionAddressWs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { createWebSocketClient } from '~/core/network/internal/createWebSocketClient';

export const refractionAddressMessages = {
ADDRESS_ASSETS: {
APPENDED: 'appended address assets',
Expand All @@ -20,10 +18,3 @@ export const refractionAddressMessages = {
REMOVED: 'removed address transactions',
},
};

export const refractionAddressWs = createWebSocketClient({
baseUrl: `${process.env.DATA_ENDPOINT}/address`,
query: {
api_token: process.env.DATA_API_KEY,
},
});
1 change: 1 addition & 0 deletions src/core/react-query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { persistOptions, queryClient } from './queryClient';
export type {
MutationConfig,
MutationFunctionResult,
InfiniteQueryConfig,
QueryConfig,
QueryFunctionArgs,
QueryFunctionResult,
Expand Down
20 changes: 20 additions & 0 deletions src/core/react-query/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {
QueryFunctionContext,
QueryKey,
UseInfiniteQueryOptions,
UseMutationOptions,
UseQueryOptions,
} from '@tanstack/react-query';
Expand Down Expand Up @@ -41,6 +42,25 @@ export type QueryConfig<
| 'onSuccess'
>;

export type InfiniteQueryConfig<TQueryFnData, TError, TData> = Pick<
UseInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
Array<string | { [key: string]: any }>
>,
| 'cacheTime'
| 'enabled'
| 'refetchInterval'
| 'retry'
| 'staleTime'
| 'select'
| 'onError'
| 'onSettled'
| 'onSuccess'
>;

export type MutationConfig<Data, Error, Variables = void> = Pick<
UseMutationOptions<Data, Error, Variables>,
'onError' | 'onMutate' | 'onSettled' | 'onSuccess'
Expand Down
4 changes: 2 additions & 2 deletions src/core/resources/_selectors/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export const selectTransactionsByDate = (
) => {
const sortedTransactions = transactions.sort((tx1, tx2) => {
if (tx1.pending && tx2.pending) return (tx2.nonce || 0) - (tx1.nonce || 0);
if (tx1.pending || tx2.pending) return -1;

if (tx1.pending) return -1;
if (tx2.pending) return 1;
if (!tx1.minedAt) return -1;
if (!tx2.minedAt) return 1;

Expand Down
225 changes: 181 additions & 44 deletions src/core/resources/assets/userAssets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { getProvider } from '@wagmi/core';
import { Address } from 'wagmi';

import { addysHttp } from '~/core/network/addys';
import {
QueryConfig,
QueryFunctionArgs,
Expand All @@ -9,21 +11,32 @@ import {
queryClient,
} from '~/core/react-query';
import { SupportedCurrencyKey } from '~/core/references';
import { ParsedAssetsDictByChain } from '~/core/types/assets';
import { ChainName } from '~/core/types/chains';
import { chainIdFromChainName } from '~/core/utils/chains';
import {
ParsedAddressAsset,
ParsedAssetsDictByChain,
ZerionAsset,
} from '~/core/types/assets';
import { ChainId } from '~/core/types/chains';
import { AddressAssetsReceivedMessage } from '~/core/types/refraction';
import {
fetchAssetBalanceViaProvider,
filterAsset,
parseAddressAsset,
} from '~/core/utils/assets';
import { SUPPORTED_CHAIN_IDS } from '~/core/utils/chains';
import { greaterThan } from '~/core/utils/numbers';
import { RainbowError, logger } from '~/logger';
import {
DAI_MAINNET_ASSET,
ETH_MAINNET_ASSET,
USDC_MAINNET_ASSET,
} from '~/test/utils';

import { fetchUserAssetsByChain } from './userAssetsByChain';

const USER_ASSETS_REFETCH_INTERVAL = 60000;
const USER_ASSETS_TIMEOUT_DURATION = 20000;
export const USER_ASSETS_STALE_INTERVAL = 30000;
const REFRACTION_SUPPORTED_CHAINS = [
ChainName.mainnet,
ChainName.optimism,
ChainName.polygon,
ChainName.arbitrum,
ChainName.bsc,
];

// ///////////////////////////////////////////////
// Query Types
Expand Down Expand Up @@ -108,50 +121,173 @@ export const userAssetsSetQueryData = ({
);
};

async function userAssetsQueryFunctionByChain({
address,
currency,
connectedToHardhat,
}: UserAssetsArgs) {
async function userAssetsQueryFunction({
queryKey: [{ address, currency, connectedToHardhat }],
}: QueryFunctionArgs<typeof userAssetsQueryKey>) {
const cache = queryClient.getQueryCache();
const cachedUserAssets = cache.find(
const cachedUserAssets = (cache.find(
userAssetsQueryKey({ address, currency, connectedToHardhat }),
)?.state?.data as ParsedAssetsDictByChain;
const getResultsForChain = async (chain: ChainName) => {
const results =
(await fetchUserAssetsByChain(
{ address, chain, currency, connectedToHardhat },
{ cacheTime: 0 },
)) || {};
const chainId = chainIdFromChainName(chain);
const cachedDataForChain = cachedUserAssets?.[chainId] || {};
return {
[chainId]:
results && Object.keys(results).length ? results : cachedDataForChain,
};
};
const queries = REFRACTION_SUPPORTED_CHAINS.map((chain) =>
getResultsForChain(chain),
);
)?.state?.data || {}) as ParsedAssetsDictByChain;
try {
const results = await Promise.all(queries);
return Object.assign({}, ...results) as ParsedAssetsDictByChain;
const url = `/${SUPPORTED_CHAIN_IDS.join(',')}/${address}/assets`;
const res = await addysHttp.get<AddressAssetsReceivedMessage>(url, {
params: {
currency: currency.toLowerCase(),
},
timeout: USER_ASSETS_TIMEOUT_DURATION,
});
const chainIdsInResponse = res?.data?.meta?.chain_ids || [];
const chainIdsWithErrorsInResponse =
res?.data?.meta?.chain_ids_with_errors || [];
const assets = res?.data?.payload?.assets || [];
if (address) {
userAssetsQueryFunctionRetryByChain({
address,
chainIds: chainIdsWithErrorsInResponse,
connectedToHardhat,
currency,
});
if (assets.length && chainIdsInResponse.length) {
const parsedAssetsDict = await parseUserAssets({
address,
assets,
chainIds: chainIdsInResponse,
connectedToHardhat,
currency,
});

return parsedAssetsDict;
}
}
return cachedUserAssets;
} catch (e) {
logger.error(new RainbowError('userAssetsQueryFunction: '), {
message: (e as Error)?.message,
});
return cachedUserAssets;
}
}

async function userAssetsQueryFunction({
queryKey: [{ address, currency, connectedToHardhat }],
}: QueryFunctionArgs<typeof userAssetsQueryKey>) {
return await userAssetsQueryFunctionByChain({
address,
currency,
connectedToHardhat,
});
type UserAssetsResult = QueryFunctionResult<typeof userAssetsQueryFunction>;

async function userAssetsQueryFunctionRetryByChain({
address,
chainIds,
connectedToHardhat,
currency,
}: {
address: Address;
chainIds: ChainId[];
connectedToHardhat: boolean;
currency: SupportedCurrencyKey;
}) {
try {
const cache = queryClient.getQueryCache();
const cachedUserAssets =
(cache.find(userAssetsQueryKey({ address, currency, connectedToHardhat }))
?.state?.data as ParsedAssetsDictByChain) || {};
const retries = [];
for (const chainIdWithError of chainIds) {
retries.push(
fetchUserAssetsByChain(
{
address,
chainId: chainIdWithError,
connectedToHardhat,
currency,
},
{ cacheTime: 0 },
),
);
}
const parsedRetries = await Promise.all(retries);
for (const parsedAssets of parsedRetries) {
const values = Object.values(parsedAssets);
if (values[0]) {
cachedUserAssets[values[0].chainId] = parsedAssets;
}
}
queryClient.setQueryData(
userAssetsQueryKey({ address, connectedToHardhat, currency }),
cachedUserAssets,
);
} catch (e) {
logger.error(new RainbowError('userAssetsQueryFunctionRetryByChain: '), {
message: (e as Error)?.message,
});
}
}

type UserAssetsResult = QueryFunctionResult<typeof userAssetsQueryFunction>;
export async function parseUserAssets({
address,
assets,
chainIds,
connectedToHardhat,
currency,
}: {
address: Address;
assets: {
quantity: string;
asset: ZerionAsset;
}[];
chainIds: ChainId[];
connectedToHardhat: boolean;
currency: SupportedCurrencyKey;
}) {
const parsedAssetsDict = chainIds.reduce(
(dict, currentChainId) => ({ ...dict, [currentChainId]: {} }),
{},
) as ParsedAssetsDictByChain;
for (const { asset, quantity } of assets) {
if (!filterAsset(asset) && greaterThan(quantity, 0)) {
const parsedAsset = parseAddressAsset({
address: asset?.asset_code,
asset,
currency,
quantity,
});
parsedAssetsDict[parsedAsset?.chainId][parsedAsset.uniqueId] =
parsedAsset;
}
}
if (connectedToHardhat) {
const provider = getProvider({ chainId: ChainId.hardhat });
// force checking for ETH if connected to hardhat
const mainnetAssets = parsedAssetsDict[ChainId.mainnet];
mainnetAssets[ETH_MAINNET_ASSET.uniqueId] = ETH_MAINNET_ASSET;
if (process.env.IS_TESTING === 'true') {
mainnetAssets[USDC_MAINNET_ASSET.uniqueId] = USDC_MAINNET_ASSET;
mainnetAssets[DAI_MAINNET_ASSET.uniqueId] = DAI_MAINNET_ASSET;
}
const mainnetBalanceRequests = Object.values(mainnetAssets).map(
async (parsedAsset) => {
if (parsedAsset.chainId !== ChainId.mainnet) return parsedAsset;
try {
const _parsedAsset = await fetchAssetBalanceViaProvider({
parsedAsset,
currentAddress: address,
currency,
provider,
});
return _parsedAsset;
} catch (e) {
return parsedAsset;
}
},
);
const newParsedMainnetAssetsByUniqueId = await Promise.all(
mainnetBalanceRequests,
);
const newMainnetAssets = newParsedMainnetAssetsByUniqueId.reduce<
Record<string, ParsedAddressAsset>
>((acc, parsedAsset) => {
acc[parsedAsset.uniqueId] = parsedAsset;
return acc;
}, {});
parsedAssetsDict[ChainId.mainnet] = newMainnetAssets;
}
return parsedAssetsDict;
}

// ///////////////////////////////////////////////
// Query Hook
Expand All @@ -171,6 +307,7 @@ export function useUserAssets<TSelectResult = UserAssetsResult>(
{
...config,
refetchInterval: USER_ASSETS_REFETCH_INTERVAL,
staleTime: USER_ASSETS_REFETCH_INTERVAL,
},
);
}
Loading

0 comments on commit 3287f09

Please sign in to comment.