From 8d84b7c6c6b4c58d51116f6c22d546ae8e526a23 Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Mon, 30 Sep 2024 12:47:25 -0400 Subject: [PATCH] Claimables [PR #1]: updates to query, types, utils, wallet screen rendering logic + wallet screen components (#6140) * updates to query, types, & wallet screen components * rm onPress * thanks greg --- .../RecyclerAssetList2/Claimable.tsx | 9 ++- .../ClaimablesListHeader.tsx | 10 +-- .../RecyclerAssetList2/core/RowRenderer.tsx | 6 +- src/helpers/buildWalletSections.tsx | 32 +++++---- src/hooks/useWalletSectionsData.ts | 8 +++ src/resources/addys/claimables/query.ts | 8 ++- src/resources/addys/claimables/types.ts | 72 ++++++++++++++++--- src/resources/addys/claimables/utils.ts | 53 +++++++++++--- src/resources/defi/PositionsQuery.ts | 8 ++- src/screens/WalletScreen/index.tsx | 6 +- 10 files changed, 158 insertions(+), 54 deletions(-) diff --git a/src/components/asset-list/RecyclerAssetList2/Claimable.tsx b/src/components/asset-list/RecyclerAssetList2/Claimable.tsx index d543fb635ab..afc758e7531 100644 --- a/src/components/asset-list/RecyclerAssetList2/Claimable.tsx +++ b/src/components/asset-list/RecyclerAssetList2/Claimable.tsx @@ -5,9 +5,13 @@ import { useClaimables } from '@/resources/addys/claimables/query'; import { FasterImageView } from '@candlefinance/faster-image'; import { ButtonPressAnimation } from '@/components/animations'; import { deviceUtils } from '@/utils'; +import Routes from '@/navigation/routesNames'; +import { ExtendedState } from './core/RawRecyclerList'; -export default function Claimable({ uniqueId }: { uniqueId: string }) { +export const Claimable = React.memo(function Claimable({ uniqueId, extendedState }: { uniqueId: string; extendedState: ExtendedState }) { const { accountAddress, nativeCurrency } = useAccountSettings(); + const { navigate } = extendedState; + const { data = [] } = useClaimables( { address: accountAddress, @@ -25,6 +29,7 @@ export default function Claimable({ uniqueId }: { uniqueId: string }) { return ( navigate(Routes.CLAIM_CLAIMABLE_PANEL, { claimable })} scaleTo={0.96} paddingHorizontal="20px" justifyContent="space-between" @@ -68,4 +73,4 @@ export default function Claimable({ uniqueId }: { uniqueId: string }) { ); -} +}); diff --git a/src/components/asset-list/RecyclerAssetList2/ClaimablesListHeader.tsx b/src/components/asset-list/RecyclerAssetList2/ClaimablesListHeader.tsx index 964a58af799..c23cfc6cde9 100644 --- a/src/components/asset-list/RecyclerAssetList2/ClaimablesListHeader.tsx +++ b/src/components/asset-list/RecyclerAssetList2/ClaimablesListHeader.tsx @@ -12,7 +12,7 @@ const AnimatedImgixImage = Animated.createAnimatedComponent(Image); const TokenFamilyHeaderAnimationDuration = 200; const TokenFamilyHeaderHeight = 48; -const ClaimablesListHeader = ({ total }: { total: string }) => { +export const ClaimablesListHeader = React.memo(function ClaimablesListHeader({ total }: { total: string }) { const { colors } = useTheme(); const { isClaimablesOpen, toggleOpenClaimables } = useOpenClaimables(); @@ -84,10 +84,4 @@ const ClaimablesListHeader = ({ total }: { total: string }) => { ); -}; - -ClaimablesListHeader.animationDuration = TokenFamilyHeaderAnimationDuration; - -ClaimablesListHeader.height = TokenFamilyHeaderHeight; - -export default ClaimablesListHeader; +}); diff --git a/src/components/asset-list/RecyclerAssetList2/core/RowRenderer.tsx b/src/components/asset-list/RecyclerAssetList2/core/RowRenderer.tsx index b5805584840..7e14631d279 100644 --- a/src/components/asset-list/RecyclerAssetList2/core/RowRenderer.tsx +++ b/src/components/asset-list/RecyclerAssetList2/core/RowRenderer.tsx @@ -35,8 +35,8 @@ import { RemoteCardCarousel } from '@/components/cards/remote-cards'; import WrappedCollectiblesHeader from '../WrappedCollectiblesHeader'; import NFTLoadingSkeleton from '../NFTLoadingSkeleton'; import { NFTEmptyState } from '../NFTEmptyState'; -import Claimable from '../Claimable'; -import ClaimablesListHeader from '../ClaimablesListHeader'; +import { ClaimablesListHeader } from '../ClaimablesListHeader'; +import { Claimable } from '../Claimable'; function rowRenderer(type: CellType, { uid }: { uid: string }, _: unknown, extendedState: ExtendedState) { const data = extendedState.additionalData[uid]; @@ -175,7 +175,7 @@ function rowRenderer(type: CellType, { uid }: { uid: string }, _: unknown, exten case CellType.CLAIMABLE: { const { uniqueId } = data as ClaimableExtraData; - return ; + return ; } case CellType.LOADING_ASSETS: diff --git a/src/helpers/buildWalletSections.tsx b/src/helpers/buildWalletSections.tsx index a612150f20d..833b154fad7 100644 --- a/src/helpers/buildWalletSections.tsx +++ b/src/helpers/buildWalletSections.tsx @@ -1,13 +1,10 @@ import { createSelector } from 'reselect'; import { buildBriefCoinsList, buildBriefUniqueTokenList } from './assets'; import { NativeCurrencyKey, ParsedAddressAsset } from '@/entities'; -import { queryClient } from '@/react-query'; -import { positionsQueryKey } from '@/resources/defi/PositionsQuery'; import store from '@/redux/store'; import { ClaimableExtraData, PositionExtraData } from '@/components/asset-list/RecyclerAssetList2/core/ViewTypes'; import { DEFI_POSITIONS, CLAIMABLES, ExperimentalValue } from '@/config/experimental'; import { RainbowPositions } from '@/resources/defi/types'; -import { claimablesQueryKey } from '@/resources/addys/claimables/query'; import { Claimable } from '@/resources/addys/claimables/types'; import { add, convertAmountToNativeDisplay } from './utilities'; import { RainbowConfig } from '@/model/remoteConfig'; @@ -60,20 +57,24 @@ const isFetchingNftsSelector = (state: any) => state.isFetchingNfts; const listTypeSelector = (state: any) => state.listType; const remoteConfigSelector = (state: any) => state.remoteConfig; const experimentalConfigSelector = (state: any) => state.experimentalConfig; +const positionsSelector = (state: any) => state.positions; +const claimablesSelector = (state: any) => state.claimables; const buildBriefWalletSections = ( balanceSectionData: any, uniqueTokenFamiliesSection: any, remoteConfig: RainbowConfig, - experimentalConfig: Record + experimentalConfig: Record, + positions: RainbowPositions | undefined, + claimables: Claimable[] | undefined ) => { const { balanceSection, isEmpty, isLoadingUserAssets } = balanceSectionData; const positionsEnabled = experimentalConfig[DEFI_POSITIONS] && !IS_TEST; const claimablesEnabled = (remoteConfig.claimables || experimentalConfig[CLAIMABLES]) && !IS_TEST; - const positionSection = positionsEnabled ? withPositionsSection(isLoadingUserAssets) : []; - const claimablesSection = claimablesEnabled ? withClaimablesSection(isLoadingUserAssets) : []; + const positionSection = positionsEnabled ? withPositionsSection(positions, isLoadingUserAssets) : []; + const claimablesSection = claimablesEnabled ? withClaimablesSection(claimables, isLoadingUserAssets) : []; const sections = [balanceSection, claimablesSection, positionSection, uniqueTokenFamiliesSection]; const filteredSections = sections.filter(section => section.length !== 0).flat(1); @@ -84,10 +85,7 @@ const buildBriefWalletSections = ( }; }; -const withPositionsSection = (isLoadingUserAssets: boolean) => { - const { accountAddress: address, nativeCurrency: currency } = store.getState().settings; - const positionsObj: RainbowPositions | undefined = queryClient.getQueryData(positionsQueryKey({ address, currency })); - +const withPositionsSection = (positionsObj: RainbowPositions | undefined, isLoadingUserAssets: boolean) => { const result: PositionExtraData[] = []; const sortedPositions = positionsObj?.positions?.sort((a, b) => (a.totals.totals.amount > b.totals.totals.amount ? -1 : 1)); sortedPositions?.forEach((position, index) => { @@ -118,9 +116,8 @@ const withPositionsSection = (isLoadingUserAssets: boolean) => { return []; }; -const withClaimablesSection = (isLoadingUserAssets: boolean) => { - const { accountAddress: address, nativeCurrency: currency } = store.getState().settings; - const claimables: Claimable[] | undefined = queryClient.getQueryData(claimablesQueryKey({ address, currency })); +const withClaimablesSection = (claimables: Claimable[] | undefined, isLoadingUserAssets: boolean) => { + const { nativeCurrency: currency } = store.getState().settings; const result: ClaimableExtraData[] = []; let totalNativeValue = '0'; @@ -285,6 +282,13 @@ const briefBalanceSectionSelector = createSelector( ); export const buildBriefWalletSectionsSelector = createSelector( - [briefBalanceSectionSelector, (state: any) => briefUniqueTokenDataSelector(state), remoteConfigSelector, experimentalConfigSelector], + [ + briefBalanceSectionSelector, + (state: any) => briefUniqueTokenDataSelector(state), + remoteConfigSelector, + experimentalConfigSelector, + positionsSelector, + claimablesSelector, + ], buildBriefWalletSections ); diff --git a/src/hooks/useWalletSectionsData.ts b/src/hooks/useWalletSectionsData.ts index f974ee2673c..91e7298151e 100644 --- a/src/hooks/useWalletSectionsData.ts +++ b/src/hooks/useWalletSectionsData.ts @@ -14,6 +14,8 @@ import useNftSort from './useNFTsSortBy'; import useWalletsWithBalancesAndNames from './useWalletsWithBalancesAndNames'; import { useRemoteConfig } from '@/model/remoteConfig'; import { RainbowContext } from '@/helpers/RainbowContext'; +import { usePositions } from '@/resources/defi/PositionsQuery'; +import { useClaimables } from '@/resources/addys/claimables/query'; export default function useWalletSectionsData({ type, @@ -35,6 +37,8 @@ export default function useWalletSectionsData({ address: accountAddress, sortBy: nftSort, }); + const { data: positions } = usePositions({ address: accountAddress, currency: nativeCurrency }); + const { data: claimables } = useClaimables({ address: accountAddress, currency: nativeCurrency }); const walletsWithBalancesAndNames = useWalletsWithBalancesAndNames(); @@ -76,6 +80,8 @@ export default function useWalletSectionsData({ nftSort, remoteConfig, experimentalConfig, + positions, + claimables, }; const { briefSectionsData, isEmpty } = buildBriefWalletSectionsSelector(accountInfo); @@ -110,6 +116,8 @@ export default function useWalletSectionsData({ nftSort, remoteConfig, experimentalConfig, + positions, + claimables, ]); return walletSections; } diff --git a/src/resources/addys/claimables/query.ts b/src/resources/addys/claimables/query.ts index 10aaa2bfa16..137a5846caa 100644 --- a/src/resources/addys/claimables/query.ts +++ b/src/resources/addys/claimables/query.ts @@ -11,8 +11,10 @@ import { CLAIMABLES, useExperimentalFlag } from '@/config'; import { IS_TEST } from '@/env'; import { SUPPORTED_CHAIN_IDS } from '@/chains'; -const addysHttp = new RainbowFetchClient({ - baseURL: 'https://addys.p.rainbow.me/v3', +export const ADDYS_BASE_URL = 'https://addys.p.rainbow.me/v3'; + +export const addysHttp = new RainbowFetchClient({ + baseURL: ADDYS_BASE_URL, headers: { Authorization: `Bearer ${ADDYS_API_KEY}`, }, @@ -30,7 +32,7 @@ export type ClaimablesArgs = { // Query Key export const claimablesQueryKey = ({ address, currency }: ClaimablesArgs) => - createQueryKey('claimables', { address, currency }, { persisterVersion: 1 }); + createQueryKey('claimables', { address, currency }, { persisterVersion: 2 }); type ClaimablesQueryKey = ReturnType; diff --git a/src/resources/addys/claimables/types.ts b/src/resources/addys/claimables/types.ts index 814a4bfc869..28538833b69 100644 --- a/src/resources/addys/claimables/types.ts +++ b/src/resources/addys/claimables/types.ts @@ -1,6 +1,6 @@ -import { ChainId } from '@rainbow-me/swaps'; import { Address } from 'viem'; import { AddysAsset, AddysConsolidatedError, AddysResponseStatus } from '../types'; +import { ChainId } from '@/chains/types'; interface Colors { primary: string; @@ -28,20 +28,33 @@ interface DApp { colors: Colors; } -type ClaimableType = 'transaction' | 'sponsored'; - -export interface AddysClaimable { +interface AddysBaseClaimable { name: string; unique_id: string; - type: ClaimableType; + type: string; network: ChainId; asset: AddysAsset; amount: string; dapp: DApp; - claim_action_type?: string | null; +} + +interface AddysTransactionClaimable extends AddysBaseClaimable { + claim_action_type: 'transaction'; + claim_action: ClaimActionTransaction[]; +} + +interface AddysSponsoredClaimable extends AddysBaseClaimable { + claim_action_type: 'sponsored'; + claim_action: ClaimActionSponsored[]; +} + +interface AddysUnsupportedClaimable extends AddysBaseClaimable { + claim_action_type?: 'unknown' | null; claim_action?: ClaimAction[]; } +export type AddysClaimable = AddysTransactionClaimable | AddysSponsoredClaimable | AddysUnsupportedClaimable; + interface ConsolidatedClaimablesPayloadResponse { claimables: AddysClaimable[]; } @@ -61,8 +74,13 @@ export interface ConsolidatedClaimablesResponse { payload: ConsolidatedClaimablesPayloadResponse; } -// will add more attributes as needed -export interface Claimable { +interface BaseClaimable { + asset: { + iconUrl: string; + name: string; + symbol: string; + }; + chainId: ChainId; name: string; uniqueId: string; iconUrl: string; @@ -71,3 +89,41 @@ export interface Claimable { nativeAsset: { amount: string; display: string }; }; } + +export interface TransactionClaimable extends BaseClaimable { + type: 'transaction'; + action: { to: Address; data: string }; +} + +export interface SponsoredClaimable extends BaseClaimable { + type: 'sponsored'; + action: { url: string; method: string }; +} + +export type Claimable = TransactionClaimable | SponsoredClaimable; + +interface ClaimTransactionStatus { + network: ChainId; + transaction_hash: string; + explorer_url: string; + sponsored_status: string; +} + +interface ClaimPayloadResponse { + success: boolean; + claimable: Claimable | null; + claim_transaction_status: ClaimTransactionStatus | null; +} + +interface ClaimMetadataResponse { + address: string; + chain_id: ChainId; + currency: string; + claim_type: string; + error: string; +} + +export interface ClaimResponse { + metadata: ClaimMetadataResponse; + payload: ClaimPayloadResponse; +} diff --git a/src/resources/addys/claimables/utils.ts b/src/resources/addys/claimables/utils.ts index 30b0395cc87..71b6122a3f1 100644 --- a/src/resources/addys/claimables/utils.ts +++ b/src/resources/addys/claimables/utils.ts @@ -1,17 +1,50 @@ import { NativeCurrencyKey } from '@/entities'; import { AddysClaimable, Claimable } from './types'; -import { convertRawAmountToBalance, convertRawAmountToNativeDisplay, greaterThan, lessThan } from '@/helpers/utilities'; +import { convertRawAmountToBalance, convertRawAmountToNativeDisplay, greaterThan } from '@/helpers/utilities'; export const parseClaimables = (claimables: AddysClaimable[], currency: NativeCurrencyKey): Claimable[] => { return claimables - .map(claimable => ({ - name: claimable.name, - uniqueId: claimable.unique_id, - iconUrl: claimable.dapp.icon_url, - value: { - claimAsset: convertRawAmountToBalance(claimable.amount, claimable.asset), - nativeAsset: convertRawAmountToNativeDisplay(claimable.amount, claimable.asset.decimals, claimable.asset.price.value, currency), - }, - })) + .map(claimable => { + if ( + !(claimable.claim_action_type === 'transaction' || claimable.claim_action_type === 'sponsored') || + !claimable.claim_action?.length + ) { + return undefined; + } + + const baseClaimable = { + asset: { + iconUrl: claimable.asset.icon_url, + name: claimable.asset.name, + symbol: claimable.asset.symbol, + }, + chainId: claimable.network, + name: claimable.name, + uniqueId: claimable.unique_id, + iconUrl: claimable.dapp.icon_url, + value: { + claimAsset: convertRawAmountToBalance(claimable.amount, claimable.asset), + nativeAsset: convertRawAmountToNativeDisplay(claimable.amount, claimable.asset.decimals, claimable.asset.price.value, currency), + }, + }; + + if (claimable.claim_action_type === 'transaction') { + return { + ...baseClaimable, + type: 'transaction' as const, + action: { + to: claimable.claim_action[0].address_to, + data: claimable.claim_action[0].calldata, + }, + }; + } else if (claimable.claim_action_type === 'sponsored') { + return { + ...baseClaimable, + type: 'sponsored' as const, + action: { method: claimable.claim_action[0].method, url: claimable.claim_action[0].url }, + }; + } + }) + .filter((c): c is Claimable => !!c) .sort((a, b) => (greaterThan(a.value.claimAsset.amount ?? '0', b.value.claimAsset.amount ?? '0') ? -1 : 1)); }; diff --git a/src/resources/defi/PositionsQuery.ts b/src/resources/defi/PositionsQuery.ts index 46e935d00ed..0bd90cffb14 100644 --- a/src/resources/defi/PositionsQuery.ts +++ b/src/resources/defi/PositionsQuery.ts @@ -8,6 +8,8 @@ import { ADDYS_API_KEY } from 'react-native-dotenv'; import { AddysPositionsResponse, PositionsArgs } from './types'; import { parsePositions } from './utils'; import { SUPPORTED_CHAIN_IDS } from '@/chains'; +import { DEFI_POSITIONS, useExperimentalFlag } from '@/config'; +import { IS_TEST } from '@/env'; export const buildPositionsUrl = (address: string) => { const networkString = SUPPORTED_CHAIN_IDS.join(','); @@ -77,5 +79,9 @@ export async function fetchPositions( // Query Hook export function usePositions({ address, currency }: PositionsArgs, config: QueryConfig = {}) { - return useQuery(positionsQueryKey({ address, currency }), positionsQueryFunction, { ...config, enabled: !!address }); + const positionsEnabled = useExperimentalFlag(DEFI_POSITIONS); + return useQuery(positionsQueryKey({ address, currency }), positionsQueryFunction, { + ...config, + enabled: !!(address && positionsEnabled && !IS_TEST), + }); } diff --git a/src/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx index 7b00c9e94e7..391dae64608 100644 --- a/src/screens/WalletScreen/index.tsx +++ b/src/screens/WalletScreen/index.tsx @@ -22,13 +22,11 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { analyticsV2 } from '@/analytics'; import { AppState } from '@/redux/store'; import { addressCopiedToastAtom } from '@/recoil/addressCopiedToastAtom'; -import { usePositions } from '@/resources/defi/PositionsQuery'; import styled from '@/styled-thing'; import { IS_ANDROID } from '@/env'; import { RemoteCardsSync } from '@/state/sync/RemoteCardsSync'; import { RemotePromoSheetSync } from '@/state/sync/RemotePromoSheetSync'; import { UserAssetsSync } from '@/state/sync/UserAssetsSync'; -import { useClaimables } from '@/resources/addys/claimables/query'; import { MobileWalletProtocolListener } from '@/components/MobileWalletProtocolListener'; import { runWalletBackupStatusChecks } from '@/handlers/walletReadyEvents'; @@ -44,9 +42,7 @@ const WalletScreen: React.FC = ({ navigation, route }) => { const removeFirst = useRemoveFirst(); const [initialized, setInitialized] = useState(!!params?.initialized); const initializeWallet = useInitializeWallet(); - const { network: currentNetwork, accountAddress, appIcon, nativeCurrency } = useAccountSettings(); - usePositions({ address: accountAddress, currency: nativeCurrency }); - useClaimables({ address: accountAddress, currency: nativeCurrency }); + const { network: currentNetwork, accountAddress, appIcon } = useAccountSettings(); const loadAccountLateData = useLoadAccountLateData(); const loadGlobalLateData = useLoadGlobalLateData();