diff --git a/packages/bento-common/utils/index.ts b/packages/bento-common/utils/index.ts index fe064eaa..2fd9033f 100644 --- a/packages/bento-common/utils/index.ts +++ b/packages/bento-common/utils/index.ts @@ -1,3 +1,3 @@ export { Base64 } from './Base64'; -export { safePromiseAll } from './safePromiseAll'; +export { safePromiseAll, safeAsyncFlatMap } from './safer-promises'; export { shortenAddress } from './shortenAddress'; diff --git a/packages/bento-common/utils/safePromiseAll.ts b/packages/bento-common/utils/safePromiseAll.ts deleted file mode 100644 index 7335b9b2..00000000 --- a/packages/bento-common/utils/safePromiseAll.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const safePromiseAll = async (promises: Promise[]) => - (await Promise.allSettled(promises)).flatMap((res) => - res.status === 'fulfilled' ? res.value : [], - ); diff --git a/packages/bento-common/utils/safer-promises.ts b/packages/bento-common/utils/safer-promises.ts new file mode 100644 index 00000000..25630afe --- /dev/null +++ b/packages/bento-common/utils/safer-promises.ts @@ -0,0 +1,17 @@ +export const safePromiseAll = async (promises: Promise[]) => + (await Promise.allSettled(promises)).flatMap((res) => + res.status === 'fulfilled' ? res.value : [], + ); + +export const safeAsyncFlatMap = async ( + array: T[], + callback: (value: T, index: number, array: T[]) => Promise<[] | U | U[]>, +): Promise => { + const result = await Promise.allSettled(array.map(callback)); + return result.flatMap((res) => { + if (res.status === 'fulfilled') { + return res.value; + } + return []; + }); +}; diff --git a/packages/bento-web/pages/api/auth/verify/[walletType].ts b/packages/bento-web/pages/api/auth/verify/[walletType].ts index c661e677..88069cec 100644 --- a/packages/bento-web/pages/api/auth/verify/[walletType].ts +++ b/packages/bento-web/pages/api/auth/verify/[walletType].ts @@ -135,9 +135,9 @@ const handler = async (req: APIRequest, res: NextApiResponse) => { .select('id, address, networks, user_id') .eq('address', walletAddress) .eq('user_id', user.id); - const previousNetworks = (prevNetworkQuery.data ?? []) - .map((v) => v.networks) - .flat(); + const previousNetworks = (prevNetworkQuery.data ?? []).flatMap( + (v) => v.networks, + ); const mergedNetworks = Array.from( new Set([...previousNetworks, ...networks]), ); diff --git a/packages/bento-web/pages/api/balances/cosmos-sdk/[network]/[walletAddress].ts b/packages/bento-web/pages/api/balances/cosmos-sdk/[network]/[walletAddress].ts index 4fd7295a..69dc3536 100644 --- a/packages/bento-web/pages/api/balances/cosmos-sdk/[network]/[walletAddress].ts +++ b/packages/bento-web/pages/api/balances/cosmos-sdk/[network]/[walletAddress].ts @@ -1,4 +1,4 @@ -import { CosmosSDKBasedNetworks, safePromiseAll } from '@bento/common'; +import { CosmosSDKBasedNetworks, safeAsyncFlatMap } from '@bento/common'; import { Bech32Address, CosmosHubChain, @@ -45,47 +45,43 @@ const handler = async (req: APIRequest, res: NextApiResponse) => { coinMarketCapId?: number; balance: number; price?: number; - }[] = ( - await safePromiseAll( - wallets.flatMap(async (walletAddress) => { - const bech32Address = Bech32Address.fromBech32(walletAddress); + }[] = await safeAsyncFlatMap(wallets, async (walletAddress) => { + const bech32Address = Bech32Address.fromBech32(walletAddress); - if (['cosmos-hub', 'osmosis'].includes(network)) { - const chain = chains[network]; - const chainBech32Address = bech32Address.toBech32( - chain.bech32Config.prefix, - ); + if (['cosmos-hub', 'osmosis'].includes(network)) { + const chain = chains[network]; + const chainBech32Address = bech32Address.toBech32( + chain.bech32Config.prefix, + ); - const getTokenBalances = async (): Promise => - 'getTokenBalances' in chain - ? chain.getTokenBalances?.(chainBech32Address) ?? [] - : []; - const [balance, delegations, tokenBalances] = await Promise.all([ - chain.getBalance(chainBech32Address).catch(() => 0), - chain.getDelegations(chainBech32Address).catch(() => 0), - getTokenBalances(), - ]); + const getTokenBalances = async (): Promise => + 'getTokenBalances' in chain + ? chain.getTokenBalances?.(chainBech32Address) ?? [] + : []; + const [balance, delegations, tokenBalances] = await Promise.all([ + chain.getBalance(chainBech32Address).catch(() => 0), + chain.getDelegations(chainBech32Address).catch(() => 0), + getTokenBalances(), + ]); - return [ - { - walletAddress: chainBech32Address, - platform: network, + return [ + { + walletAddress: chainBech32Address, + platform: network, - symbol: chain.currency.symbol, - name: chain.currency.name, - logo: chain.currency.logo, - coinGeckoId: chain.currency.coinGeckoId, - balance, - delegations, - price: undefined, - }, - ...tokenBalances, - ]; - } - return []; - }), - ) - ).flat(); + symbol: chain.currency.symbol, + name: chain.currency.name, + logo: chain.currency.logo, + coinGeckoId: chain.currency.coinGeckoId, + balance, + delegations, + price: undefined, + }, + ...tokenBalances, + ]; + } + return []; + }); res.status(200).json(result); }; diff --git a/packages/bento-web/pages/api/balances/evm/[network]/[walletAddress].ts b/packages/bento-web/pages/api/balances/evm/[network]/[walletAddress].ts index 5a48c74a..151a2fd4 100644 --- a/packages/bento-web/pages/api/balances/evm/[network]/[walletAddress].ts +++ b/packages/bento-web/pages/api/balances/evm/[network]/[walletAddress].ts @@ -1,4 +1,4 @@ -import { EVMBasedNetworks, safePromiseAll } from '@bento/common'; +import { EVMBasedNetworks, safeAsyncFlatMap } from '@bento/common'; import { AvalancheChain, BNBChain, @@ -56,65 +56,61 @@ const handler = async (req: APIRequest, res: NextApiResponse) => { coinMarketCapId?: number; balance: number; price?: number; - }[] = ( - await safePromiseAll( - wallets.map(async (walletAddress) => { - if (SUPPORTED_CHAINS.includes(network)) { - const chain = chains[network]!; - const getTokenBalances = async (): Promise => { - try { - const items = - 'getTokenBalances' in chain - ? await chain.getTokenBalances(walletAddress) - : []; - await redisClient - .set( - `token-balances:${network}:${walletAddress}`, - JSON.stringify(items), - ) - .catch((err) => console.error(err)); - return items; - } catch (error) { - console.error( - 'Error occurred while fetching token balances (likely Covalent API error)', - error, - ); - const out = await redisClient - .get(`token-balances:${network}:${walletAddress}`) - .catch((err) => { - console.error(err); - }); - if (out) { - return JSON.parse(out); - } - return []; - } - }; + }[] = await safeAsyncFlatMap(wallets, async (walletAddress) => { + if (SUPPORTED_CHAINS.includes(network)) { + const chain = chains[network]!; + const getTokenBalances = async (): Promise => { + try { + const items = + 'getTokenBalances' in chain + ? await chain.getTokenBalances(walletAddress) + : []; + await redisClient + .set( + `token-balances:${network}:${walletAddress}`, + JSON.stringify(items), + ) + .catch((err) => console.error(err)); + return items; + } catch (error) { + console.error( + 'Error occurred while fetching token balances (likely Covalent API error)', + error, + ); + const out = await redisClient + .get(`token-balances:${network}:${walletAddress}`) + .catch((err) => { + console.error(err); + }); + if (out) { + return JSON.parse(out); + } + return []; + } + }; - const [balance, tokenBalances] = await Promise.all([ - chain.getBalance(walletAddress).catch(() => 0), - getTokenBalances().catch(() => []), - ]); + const [balance, tokenBalances] = await Promise.all([ + chain.getBalance(walletAddress).catch(() => 0), + getTokenBalances().catch(() => []), + ]); - return [ - { - walletAddress, - platform: network, + return [ + { + walletAddress, + platform: network, - symbol: chain.currency.symbol, - name: chain.currency.name, - logo: chain.currency.logo, - coinGeckoId: chain.currency.coinGeckoId, - balance, - // price: currencyPrice, - }, - ...tokenBalances, - ]; - } - return []; - }), - ) - ).flat(); + symbol: chain.currency.symbol, + name: chain.currency.name, + logo: chain.currency.logo, + coinGeckoId: chain.currency.coinGeckoId, + balance, + // price: currencyPrice, + }, + ...tokenBalances, + ]; + } + return []; + }); const coinMarketCapIds = result .flatMap((x) => (!!x.coinMarketCapId ? x.coinMarketCapId : [])) diff --git a/packages/bento-web/pages/api/balances/solana/[network]/[walletAddress].ts b/packages/bento-web/pages/api/balances/solana/[network]/[walletAddress].ts index 93a152b9..48755ac3 100644 --- a/packages/bento-web/pages/api/balances/solana/[network]/[walletAddress].ts +++ b/packages/bento-web/pages/api/balances/solana/[network]/[walletAddress].ts @@ -1,4 +1,4 @@ -import { safePromiseAll } from '@bento/common'; +import { safeAsyncFlatMap } from '@bento/common'; import { SolanaChain, TokenBalance } from '@bento/core'; import type { NextApiRequest, NextApiResponse } from 'next'; @@ -36,62 +36,55 @@ const handler = async (req: APIRequest, res: NextApiResponse) => { coinMarketCapId?: number; balance: number; price?: number; - }[] = ( - await safePromiseAll( - wallets.map(async (walletAddress) => { - const getTokenBalances = async (): Promise => { - try { - const items = - 'getTokenBalances' in chain - ? await chain.getTokenBalances(walletAddress) - : []; - await redisClient - .set( - `token-balances:solana:${walletAddress}`, - JSON.stringify(items), - ) - .catch((err) => console.error(err)); - return items; - } catch (error) { - console.error( - 'Error occurred while fetching token balances (likely Covalent API error)', - error, - ); - const out = await redisClient - .get(`token-balances:solana:${walletAddress}`) - .catch((err) => { - console.error(err); - }); - if (out) { - return JSON.parse(out); - } - return []; - } - }; + }[] = await safeAsyncFlatMap(wallets, async (walletAddress) => { + const getTokenBalances = async (): Promise => { + try { + const items = + 'getTokenBalances' in chain + ? await chain.getTokenBalances(walletAddress) + : []; + await redisClient + .set(`token-balances:solana:${walletAddress}`, JSON.stringify(items)) + .catch((err) => console.error(err)); + return items; + } catch (error) { + console.error( + 'Error occurred while fetching token balances (likely Covalent API error)', + error, + ); + const out = await redisClient + .get(`token-balances:solana:${walletAddress}`) + .catch((err) => { + console.error(err); + }); + if (out) { + return JSON.parse(out); + } + return []; + } + }; - const [balance, tokenBalances] = await Promise.all([ - chain.getBalance(walletAddress).catch(() => 0), - getTokenBalances().catch(() => []), - [], - ]); + const [balance, tokenBalances] = await Promise.all([ + chain.getBalance(walletAddress).catch(() => 0), + getTokenBalances().catch(() => []), + [], + ]); - return [ - { - walletAddress, - platform: 'solana', + return [ + { + walletAddress, + platform: 'solana', - symbol: chain.currency.symbol, - name: chain.currency.name, - logo: chain.currency.logo, - coinGeckoId: chain.currency.coinGeckoId, - balance, - price: undefined, - }, - ...tokenBalances, - ]; - }), - ) - ).flat(); + symbol: chain.currency.symbol, + name: chain.currency.name, + logo: chain.currency.logo, + coinGeckoId: chain.currency.coinGeckoId, + balance, + price: undefined, + }, + ...tokenBalances, + ]; + }); await redisClient.disconnect(); res.status(200).json(result); diff --git a/packages/bento-web/pages/api/defis/klaytn/[walletAddress].ts b/packages/bento-web/pages/api/defis/klaytn/[walletAddress].ts index 24978bfc..b773e738 100644 --- a/packages/bento-web/pages/api/defis/klaytn/[walletAddress].ts +++ b/packages/bento-web/pages/api/defis/klaytn/[walletAddress].ts @@ -1,3 +1,4 @@ +import { safeAsyncFlatMap, safePromiseAll } from '@bento/common'; import { getTokenBalancesFromCovalent } from '@bento/core'; import { NextApiRequest, NextApiResponse } from 'next'; @@ -12,15 +13,6 @@ import { import { KlayStation } from '@/defi/klaystation'; import { KlaySwap } from '@/defi/klayswap'; import { KokonutSwap } from '@/defi/kokonutswap'; -import { DeFiStaking } from '@/defi/types/staking'; - -const asyncFlatMap = async ( - array: T[], - callback: (value: T, index: number, array: T[]) => Promise<[] | U | U[]>, -): Promise => { - const result = await Promise.all(array.map(callback)); - return result.flat() as U[]; -}; interface APIRequest extends NextApiRequest { query: { @@ -58,7 +50,7 @@ const handler = async (req: APIRequest, res: NextApiResponse) => { KlaySwap.getLeveragePoolList().catch(() => undefined), ]); - const promisesForStakings = asyncFlatMap(tokenBalances, async (token) => { + const promisesForStakings = safeAsyncFlatMap(tokenBalances, async (token) => { if (token.balance === null) { // Indexed at least once return []; @@ -123,7 +115,7 @@ const handler = async (req: APIRequest, res: NextApiResponse) => { const promisesForDelegations = KlayStation.getDelegations(walletAddress); const stakings = ( - await Promise.all([ + await safePromiseAll([ promisesForStakings.catch(handleError), promisesForDelegations.catch(handleError), ]) diff --git a/packages/bento-web/src/dashboard/utils/useNFTBalances.ts b/packages/bento-web/src/dashboard/utils/useNFTBalances.ts index 9801bef7..3f36e8b8 100644 --- a/packages/bento-web/src/dashboard/utils/useNFTBalances.ts +++ b/packages/bento-web/src/dashboard/utils/useNFTBalances.ts @@ -1,4 +1,4 @@ -import { Wallet, safePromiseAll } from '@bento/common'; +import { Wallet, safeAsyncFlatMap, safePromiseAll } from '@bento/common'; import { OpenSea, OpenSeaAsset } from '@bento/core'; import { priceFromCoinGecko } from '@bento/core'; import chunk from 'lodash.chunk'; @@ -72,8 +72,9 @@ export const useNFTBalances = ({ wallets }: Options) => { .flat(3); const groupedByWalletAddress = groupBy(flattedAssets, 'walletAddress'); - safePromiseAll( - Object.keys(groupedByWalletAddress).map(async (walletAddress) => { + safeAsyncFlatMap( + Object.keys(groupedByWalletAddress), + async (walletAddress) => { const groupByCollection: Record< string, (OpenSeaAsset & { walletAddress: string })[] @@ -83,49 +84,21 @@ export const useNFTBalances = ({ wallets }: Options) => { ); const balances: NFTWalletBalance[] = ( - await safePromiseAll( - chunk(Object.keys(groupByCollection), CHUNK_SIZE).map( - async (chunckedCollectionSlugs) => - safePromiseAll( - chunckedCollectionSlugs.map(async (collectionSlug) => { - const assets = groupByCollection[collectionSlug]; - const first = assets[0]; - const collection = first.collection; - - const { - floor_price: floorPrice, - total_volume: totalVolume, - } = await OpenSea.getCollectionStats(collectionSlug).catch( - (error) => { - console.error(error); - // FIXME: Error handling - return { floor_price: 0, total_volume: 0 }; - }, - ); - - return { - symbol: first.asset_contract.symbol || null, - name: collection.name, - walletAddress, - balance: groupByCollection[collectionSlug].length, - logo: collection.image_url, - price: ethereumPrice * floorPrice, - totalVolume, - type: 'nft' as const, - platform: 'opensea', - assets, - }; - }), - ), - ), + await safeAsyncFlatMap( + chunk(Object.keys(groupByCollection), CHUNK_SIZE), + async (chunckedCollectionSlugs) => + getNFTBalancesFromSlugs({ + slugs: chunckedCollectionSlugs, + walletAddress, + ethereumPrice, + groupByCollection, + }), ) - ) - .flat() - .filter((v) => v.totalVolume > 0); + ).filter((v) => v.totalVolume > 0); return balances; - }), - ).then((v) => setOpenSeaNFTBalance(v.flat())); + }, + ).then(setOpenSeaNFTBalance); }, [JSON.stringify(fetchedAssets)]); return { @@ -133,3 +106,48 @@ export const useNFTBalances = ({ wallets }: Options) => { jsonKey: JSON.stringify(openSeaNFTBalance), }; }; + +type Props = { + slugs: string[]; + walletAddress: string; + ethereumPrice: number; + groupByCollection: Record< + string, + (OpenSeaAsset & { + walletAddress: string; + })[] + >; +}; +const getNFTBalancesFromSlugs = ({ + slugs, + walletAddress, + ethereumPrice, + groupByCollection, +}: Props) => + safePromiseAll( + slugs.map(async (collectionSlug) => { + const assets = groupByCollection[collectionSlug]; + const first = assets[0]; + const collection = first.collection; + + const { floor_price: floorPrice, total_volume: totalVolume } = + await OpenSea.getCollectionStats(collectionSlug).catch((error) => { + console.error(error); + // FIXME: Error handling + return { floor_price: 0, total_volume: 0 }; + }); + + return { + symbol: first.asset_contract.symbol || null, + name: collection.name, + walletAddress, + balance: groupByCollection[collectionSlug].length, + logo: collection.image_url, + price: ethereumPrice * floorPrice, + totalVolume, + type: 'nft' as const, + platform: 'opensea', + assets, + }; + }), + ); diff --git a/packages/bento-web/src/profile/instance/ProfileInstance.tsx b/packages/bento-web/src/profile/instance/ProfileInstance.tsx index 4bf97ba3..892164c6 100644 --- a/packages/bento-web/src/profile/instance/ProfileInstance.tsx +++ b/packages/bento-web/src/profile/instance/ProfileInstance.tsx @@ -2,7 +2,7 @@ import { Wallet } from '@bento/common'; import { OpenSeaAsset } from '@bento/core'; import axios, { AxiosError } from 'axios'; import dedent from 'dedent'; -import { AnimatePresence, HTMLMotionProps, motion } from 'framer-motion'; +import { AnimatePresence } from 'framer-motion'; import groupBy from 'lodash.groupby'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router';