diff --git a/backend/app/infrastructure/routes/user.py b/backend/app/infrastructure/routes/user.py index 0b8eeb0d81..181e794240 100644 --- a/backend/app/infrastructure/routes/user.py +++ b/backend/app/infrastructure/routes/user.py @@ -15,7 +15,7 @@ post_user_terms_of_service_consent, get_user_terms_of_service_consent_status, ) -from app.modules.user.winnings.controller import get_user_winnings +from app.modules.user.sablier_streams.controller import get_sablier_streams from app.settings import config ns = Namespace("user", description="Octant user settings") @@ -98,8 +98,8 @@ }, ) -user_winning_model = api.model( - "UserWinning", +user_stream_model = api.model( + "UserStream", { "amount": fields.String( required=True, @@ -107,18 +107,26 @@ ), "dateAvailableForWithdrawal": fields.String( required=True, - description="Date when winning is available for withdrawal as unix timestamp", + description="Date when stream is available for withdrawal as unix timestamp", + ), + "isCancelled": fields.Boolean( + required=True, + description="Flag indicating whether stream is cancelled", + ), + "remainingAmount": fields.String( + required=True, + description="Remaining amount in WEI", ), }, ) -user_winnings_model = api.model( - "UserWinnings", +user_streams_model = api.model( + "SablierStreams", { - "winnings": fields.List( - fields.Nested(user_winning_model), + "sablierStreams": fields.List( + fields.Nested(user_stream_model), required=True, - description="User winnings", + description="User's sablier streams", ), }, ) @@ -344,29 +352,33 @@ def get(self, epoch: int): } -@ns.route("//raffle/winnings") +@ns.route("//sablier-streams") @ns.doc( params={ "user_address": "User ethereum address in hexadecimal format (case-insensitive, prefixed with 0x)", } ) -class UserWinnings(OctantResource): +class SablierStreams(OctantResource): @ns.doc( - description="Returns an array of user's winnings with amounts and availability dates", + description="Returns an array of user's streams from Sablier with amounts, availability dates, remainingAmount and isCancelled flag.", ) - @ns.marshal_with(user_winnings_model) - @ns.response(200, "User's winnings retrieved successfully") + @ns.marshal_with(user_streams_model) + @ns.response(200, "User's streams from Sablier retrieved successfully") def get(self, user_address: str): - app.logger.debug(f"Getting winnings for user {user_address}.") - winnings = get_user_winnings(user_address) - app.logger.debug(f"Retrieved {len(winnings)} winnings for user {user_address}.") + app.logger.debug(f"Getting sablier streams for user {user_address}.") + sablier_streams = get_sablier_streams(user_address) + app.logger.debug( + f"Retrieved {len(sablier_streams)} sablier streams for user {user_address}." + ) return { - "winnings": [ + "sablierStreams": [ { - "amount": winning.amount, - "dateAvailableForWithdrawal": winning.date_available_for_withdrawal, + "amount": stream.amount, + "dateAvailableForWithdrawal": stream.date_available_for_withdrawal, + "isCancelled": stream.is_cancelled, + "remainingAmount": stream.remaining_amount, } - for winning in winnings + for stream in sablier_streams ] } diff --git a/backend/app/infrastructure/sablier/events.py b/backend/app/infrastructure/sablier/events.py index 4c1f3aa3ee..8c492ee637 100644 --- a/backend/app/infrastructure/sablier/events.py +++ b/backend/app/infrastructure/sablier/events.py @@ -22,13 +22,9 @@ class SablierStream(TypedDict): id: str actions: List[SablierAction] intactAmount: str - - -class SablierStreamForTrackingWinner(TypedDict): - id: str + canceled: bool endTime: str depositAmount: str - intactAmount: str def fetch_streams(query: str, variables: Dict) -> List[SablierStream]: @@ -55,8 +51,18 @@ def fetch_streams(query: str, variables: Dict) -> List[SablierStream]: for stream in streams: actions = stream.get("actions", []) final_intact_amount = stream.get("intactAmount", 0) + is_cancelled = stream.get("canceled") + end_time = stream.get("endTime") + deposit_amount = stream.get("depositAmount") + all_streams.append( - SablierStream(actions=actions, intactAmount=final_intact_amount) + SablierStream( + actions=actions, + intactAmount=final_intact_amount, + canceled=is_cancelled, + endTime=end_time, + depositAmount=deposit_amount, + ) ) if len(streams) < limit: @@ -70,6 +76,7 @@ def fetch_streams(query: str, variables: Dict) -> List[SablierStream]: def get_user_events_history(user_address: str) -> List[SablierStream]: """ Get all the locks and unlocks for a user. + Query used for computing user's effective deposit and getting all sablier streams from an endpoint. """ query = """ query GetEvents($sender: String!, $recipient: String!, $tokenAddress: String!, $limit: Int!, $skip: Int!) { @@ -86,6 +93,9 @@ def get_user_events_history(user_address: str) -> List[SablierStream]: ) { id intactAmount + canceled + endTime + depositAmount actions(where: {category_in: [Cancel, Withdraw, Create]}, orderBy: timestamp) { category addressA @@ -146,40 +156,6 @@ def get_all_streams_history() -> List[SablierStream]: return fetch_streams(query, variables) -def get_streams_with_create_events_to_user( - user_address: str, -) -> List[SablierStreamForTrackingWinner]: - """ - Get all the create events for a user. - """ - query = """ - query GetCreateEvents($sender: String!, $recipient: String!, $tokenAddress: String!) { - streams( - where: { - sender: $sender - recipient: $recipient - asset_: {address: $tokenAddress} - transferable: false - } - orderBy: timestamp - ) { - id - intactAmount - endTime - depositAmount - } - } - """ - variables = { - "sender": _get_sender(), - "recipient": user_address, - "tokenAddress": _get_token_address(), - } - - result = gql_sablier_factory.build().execute(gql(query), variable_values=variables) - return result.get("streams", []) - - def _get_sender(): chain_id = app.config["CHAIN_ID"] sender = ( diff --git a/backend/app/modules/modules_factory/current.py b/backend/app/modules/modules_factory/current.py index 8911bc78f0..4fab679d67 100644 --- a/backend/app/modules/modules_factory/current.py +++ b/backend/app/modules/modules_factory/current.py @@ -6,7 +6,6 @@ from app.modules.history.service.full import FullHistory from app.modules.modules_factory.protocols import ( OctantRewards, - WinningsService, UserEffectiveDeposits, TotalEffectiveDeposits, HistoryService, @@ -17,6 +16,7 @@ ScoreDelegation, UniquenessQuotients, ProjectsDetailsService, + SablierStreamsService, ) from app.modules.modules_factory.protocols import SimulatePendingSnapshots from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures @@ -57,7 +57,9 @@ from app.modules.projects.details.service.projects_details import ( StaticProjectsDetailsService, ) -from app.modules.user.winnings.service.raffle import RaffleWinningsService +from app.modules.user.sablier_streams.service.sablier_streams import ( + UserSablierStreamsService, +) class CurrentUserDeposits(UserEffectiveDeposits, TotalEffectiveDeposits, Protocol): @@ -70,7 +72,7 @@ class CurrentServices(Model): user_tos_service: UserTos user_antisybil_service: GitcoinPassportAntisybil octant_rewards_service: OctantRewards - user_winnings_service: WinningsService + sablier_streams_service: SablierStreamsService history_service: HistoryService simulated_pending_snapshot_service: SimulatePendingSnapshots multisig_signatures_service: MultisigSignatures @@ -168,7 +170,7 @@ def create(chain_id: int) -> "CurrentServices": multisig_signatures_service=multisig_signatures, user_tos_service=user_tos, user_antisybil_service=user_antisybil_service, - user_winnings_service=RaffleWinningsService(), + sablier_streams_service=UserSablierStreamsService(), projects_metadata_service=StaticProjectsMetadataService(), projects_details_service=StaticProjectsDetailsService(), user_budgets_service=user_budgets, diff --git a/backend/app/modules/modules_factory/pre_pending.py b/backend/app/modules/modules_factory/pre_pending.py index 1f6fe92440..e886c4d0b0 100644 --- a/backend/app/modules/modules_factory/pre_pending.py +++ b/backend/app/modules/modules_factory/pre_pending.py @@ -10,7 +10,7 @@ AllUserEffectiveDeposits, OctantRewards, PendingSnapshots, - WinningsService, + SablierStreamsService, UserEffectiveDeposits, SavedProjectRewardsService, ProjectsMetadataService, @@ -33,7 +33,9 @@ from app.modules.projects.details.service.projects_details import ( StaticProjectsDetailsService, ) -from app.modules.user.winnings.service.raffle import RaffleWinningsService +from app.modules.user.sablier_streams.service.sablier_streams import ( + UserSablierStreamsService, +) class PrePendingUserDeposits(UserEffectiveDeposits, AllUserEffectiveDeposits, Protocol): @@ -47,7 +49,7 @@ class PrePendingServices(Model): project_rewards_service: SavedProjectRewardsService projects_metadata_service: ProjectsMetadataService projects_details_service: ProjectsDetailsService - user_winnings_service: WinningsService + user_winnings_service: SablierStreamsService @staticmethod def create(chain_id: int) -> "PrePendingServices": @@ -83,5 +85,5 @@ def create(chain_id: int) -> "PrePendingServices": project_rewards_service=SavedProjectRewards(), projects_metadata_service=StaticProjectsMetadataService(), projects_details_service=StaticProjectsDetailsService(), - user_winnings_service=RaffleWinningsService(), + user_winnings_service=UserSablierStreamsService(), ) diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index a19788e0f1..70a158ddd2 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -20,7 +20,7 @@ from app.modules.history.dto import UserHistoryDTO from app.modules.multisig_signatures.dto import Signature from app.modules.projects.details.service.projects_details import ProjectsDetailsDTO -from app.modules.user.winnings.service.raffle import UserWinningDTO +from app.modules.user.sablier_streams.service.sablier_streams import UserStreamsDTO @runtime_checkable @@ -127,10 +127,10 @@ def get_unused_rewards(self, context: Context) -> Dict[str, int]: @runtime_checkable -class WinningsService(Protocol): - def get_user_winnings( +class SablierStreamsService(Protocol): + def get_sablier_streams( self, context: Context, user_address: str - ) -> List[UserWinningDTO]: + ) -> List[UserStreamsDTO]: ... diff --git a/backend/app/modules/user/sablier_streams/controller.py b/backend/app/modules/user/sablier_streams/controller.py new file mode 100644 index 0000000000..fc910b538f --- /dev/null +++ b/backend/app/modules/user/sablier_streams/controller.py @@ -0,0 +1,16 @@ +from typing import List + +from app.context.manager import state_context +from app.modules.registry import get_services +from app.modules.modules_factory.protocols import SablierStreamsService +from app.modules.user.sablier_streams.service.sablier_streams import UserStreamsDTO +from app.context.epoch_state import EpochState + + +def get_sablier_streams(user_address: str) -> List[UserStreamsDTO]: + context = state_context(EpochState.CURRENT) + service: SablierStreamsService = get_services( + context.epoch_state + ).sablier_streams_service + + return service.get_sablier_streams(context, user_address) diff --git a/backend/app/modules/user/sablier_streams/service/sablier_streams.py b/backend/app/modules/user/sablier_streams/service/sablier_streams.py new file mode 100644 index 0000000000..06a8bd9e74 --- /dev/null +++ b/backend/app/modules/user/sablier_streams/service/sablier_streams.py @@ -0,0 +1,41 @@ +from app.pydantic import Model +from typing import List +from dataclasses import dataclass + +from app.infrastructure.sablier.events import get_user_events_history +from app.context.manager import Context + + +@dataclass +class UserStreamsDTO: + amount: str + date_available_for_withdrawal: str + is_cancelled: bool + remaining_amount: str + + +class UserSablierStreamsService(Model): + def get_sablier_streams( + self, _: Context, user_address: str + ) -> List[UserStreamsDTO]: + user_streams = get_user_events_history( + user_address + ) # in practice: we should assume a user only has always one stream (one create event) + + user_streams_details = [] + for user_stream in user_streams: + date_available_for_withdrawal = user_stream["endTime"] + amount = user_stream["depositAmount"] + is_cancelled = user_stream["canceled"] + remaining_amount = user_stream["intactAmount"] + + user_streams_details.append( + UserStreamsDTO( + amount=amount, + date_available_for_withdrawal=date_available_for_withdrawal, + is_cancelled=is_cancelled, + remaining_amount=remaining_amount, + ) + ) + + return user_streams_details diff --git a/backend/app/modules/user/winnings/controller.py b/backend/app/modules/user/winnings/controller.py deleted file mode 100644 index 63198ba635..0000000000 --- a/backend/app/modules/user/winnings/controller.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import List - -from app.context.manager import state_context -from app.modules.registry import get_services -from app.modules.modules_factory.protocols import WinningsService -from app.modules.user.winnings.service.raffle import UserWinningDTO -from app.context.epoch_state import EpochState - - -def get_user_winnings(user_address: str) -> List[UserWinningDTO]: - context = state_context(EpochState.CURRENT) - service: WinningsService = get_services(context.epoch_state).user_winnings_service - - return service.get_user_winnings(context, user_address) diff --git a/backend/app/modules/user/winnings/service/raffle.py b/backend/app/modules/user/winnings/service/raffle.py deleted file mode 100644 index 03d0c1e2f0..0000000000 --- a/backend/app/modules/user/winnings/service/raffle.py +++ /dev/null @@ -1,30 +0,0 @@ -from app.pydantic import Model -from typing import List -from dataclasses import dataclass - -from app.infrastructure.sablier.events import get_streams_with_create_events_to_user -from app.context.manager import Context - - -@dataclass -class UserWinningDTO: - amount: str - date_available_for_withdrawal: str - - -class RaffleWinningsService(Model): - def get_user_winnings(self, _: Context, user_address: str) -> List[UserWinningDTO]: - streams = get_streams_with_create_events_to_user(user_address) - user_winnings = [] - - for stream in streams: - date_available_for_withdrawal = stream["endTime"] - amount = stream["depositAmount"] - user_winnings.append( - UserWinningDTO( - amount=amount, - date_available_for_withdrawal=date_available_for_withdrawal, - ) - ) - - return user_winnings diff --git a/backend/tests/helpers/gql_client.py b/backend/tests/helpers/gql_client.py index 0413b04715..29c67e9db5 100644 --- a/backend/tests/helpers/gql_client.py +++ b/backend/tests/helpers/gql_client.py @@ -247,6 +247,7 @@ def _prepare_payload(self, recipient): "transferable": False, "endTime": "1729077035", "depositAmount": "10000000000000000000", + "canceled": True, } ] } diff --git a/ci/argocd/contracts/uat.env b/ci/argocd/contracts/uat.env index 842831d991..d9341b3bb2 100644 --- a/ci/argocd/contracts/uat.env +++ b/ci/argocd/contracts/uat.env @@ -1,8 +1,8 @@ -BLOCK_NUMBER=7446089 +BLOCK_NUMBER=7497637 GLM_CONTRACT_ADDRESS=0x71432DD1ae7DB41706ee6a22148446087BdD0906 -AUTH_CONTRACT_ADDRESS=0x6929e60755d9E53bd9136003646F91b224548b36 -DEPOSITS_CONTRACT_ADDRESS=0x0fbf5f094B8E3C7EB136aD83d2F322d7C506217b -EPOCHS_CONTRACT_ADDRESS=0x10E47B1fF29D23fA52a0B14053C02Bc01D47bf24 -PROPOSALS_CONTRACT_ADDRESS=0x8d2B677e9D7bFb1512e48ECAA89Dd554807c83F5 -WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0xe0458eD4e638C7DEcF929379565ddcdf5B439A50 -VAULT_CONTRACT_ADDRESS=0xc0f1C4CE40F528cA346287C6F5f46491B5Ef901d +AUTH_CONTRACT_ADDRESS=0xb2Fd30999060C106dA03eb8dFF30D7E404a581b7 +DEPOSITS_CONTRACT_ADDRESS=0xC35b4D9231144aE08598e3CcC9C2496F1e858e52 +EPOCHS_CONTRACT_ADDRESS=0xdC5A8fae5608bB49F584f5D11C9b56862C08E3C1 +PROPOSALS_CONTRACT_ADDRESS=0x93Ce86C64bd089399c3830D4364B30f07B4d4012 +WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0x2c17c8C2AAc7efa5379a83AABcEa43B8E6f424be +VAULT_CONTRACT_ADDRESS=0x439D1fce728D068528E2626F376b38db8DA7F8a4 diff --git a/client/src/api/calls/userWinnings.ts b/client/src/api/calls/userSablierStreams.ts similarity index 54% rename from client/src/api/calls/userWinnings.ts rename to client/src/api/calls/userSablierStreams.ts index 6c1af2e690..63524160bd 100644 --- a/client/src/api/calls/userWinnings.ts +++ b/client/src/api/calls/userSablierStreams.ts @@ -2,14 +2,16 @@ import env from 'env'; import apiService from 'services/apiService'; export type Response = { - winnings: { + sablierStreams: { amount: string; dateAvailableForWithdrawal: string; + isCancelled: boolean; + remainingAmount: string; }[]; }; -export async function apiGetUserRaffleWinnings(address: string): Promise { +export async function apiGetUserSablierStreams(address: string): Promise { return apiService - .get(`${env.serverEndpoint}user/${address}/raffle/winnings`) + .get(`${env.serverEndpoint}user/${address}/sablier-streams`) .then(({ data }) => data); } diff --git a/client/src/api/queryKeys/index.ts b/client/src/api/queryKeys/index.ts index fe480a2ff6..6b5e63f6ac 100644 --- a/client/src/api/queryKeys/index.ts +++ b/client/src/api/queryKeys/index.ts @@ -22,8 +22,8 @@ export const ROOTS: Root = { projectsDonors: 'projectsDonors', projectsEpoch: 'projectsEpoch', projectsIpfsResults: 'projectsIpfsResults', - raffleWinnings: 'raffleWinnings', rewardsRate: 'rewardsRate', + sablierStreams: 'sablierStreams', searchResultsDetails: 'searchResultsDetails', upcomingBudget: 'upcomingBudget', uqScore: 'uqScore', @@ -75,8 +75,8 @@ export const QUERY_KEYS: QueryKeys = { ], projectsMetadataAccumulateds: ['projectsMetadataAccumulateds'], projectsMetadataPerEpoches: ['projectsMetadataPerEpoches'], - raffleWinnings: userAddress => [ROOTS.raffleWinnings, userAddress], rewardsRate: epochNumber => [ROOTS.rewardsRate, epochNumber.toString()], + sablierStreams: userAddress => [ROOTS.sablierStreams, userAddress], searchResults: ['searchResults'], searchResultsDetails: (address, epoch) => [ROOTS.searchResultsDetails, address, epoch.toString()], syncStatus: ['syncStatus'], diff --git a/client/src/api/queryKeys/types.ts b/client/src/api/queryKeys/types.ts index 1fc076887a..590d47f0c3 100644 --- a/client/src/api/queryKeys/types.ts +++ b/client/src/api/queryKeys/types.ts @@ -22,8 +22,8 @@ export type Root = { projectsDonors: 'projectsDonors'; projectsEpoch: 'projectsEpoch'; projectsIpfsResults: 'projectsIpfsResults'; - raffleWinnings: 'raffleWinnings'; rewardsRate: 'rewardsRate'; + sablierStreams: 'sablierStreams'; searchResultsDetails: 'searchResultsDetails'; upcomingBudget: 'upcomingBudget'; uqScore: 'uqScore'; @@ -75,8 +75,8 @@ export type QueryKeys = { ) => [Root['projectsIpfsResults'], string, string]; projectsMetadataAccumulateds: ['projectsMetadataAccumulateds']; projectsMetadataPerEpoches: ['projectsMetadataPerEpoches']; - raffleWinnings: (userAddress: string) => [Root['raffleWinnings'], string]; rewardsRate: (epochNumber: number) => [Root['rewardsRate'], string]; + sablierStreams: (userAddress: string) => [Root['sablierStreams'], string]; searchResults: ['searchResults']; searchResultsDetails: ( address: string, diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/HomeGridCurrentGlmLock.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/HomeGridCurrentGlmLock.tsx index 8367a51ae5..c25447993a 100644 --- a/client/src/components/Home/HomeGridCurrentGlmLock/HomeGridCurrentGlmLock.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/HomeGridCurrentGlmLock.tsx @@ -1,5 +1,5 @@ import _first from 'lodash/first'; -import React, { FC, useState } from 'react'; +import React, { FC, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAccount } from 'wagmi'; @@ -12,7 +12,7 @@ import useMediaQuery from 'hooks/helpers/useMediaQuery'; import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; import useDepositValue from 'hooks/queries/useDepositValue'; import useEstimatedEffectiveDeposit from 'hooks/queries/useEstimatedEffectiveDeposit'; -import useUserRaffleWinnings from 'hooks/queries/useUserRaffleWinnings'; +import useUserSablierStreams from 'hooks/queries/useUserSablierStreams'; import useTransactionLocalStore from 'store/transactionLocal/store'; import getIsPreLaunch from 'utils/getIsPreLaunch'; @@ -38,11 +38,26 @@ const HomeGridCurrentGlmLock: FC = ({ className }) const { data: estimatedEffectiveDeposit, isFetching: isFetchingEstimatedEffectiveDeposit } = useEstimatedEffectiveDeposit(); const { data: depositsValue, isFetching: isFetchingDepositValue } = useDepositValue(); - const { data: userRaffleWinnings, isFetching: isFetchingUserRaffleWinnings } = - useUserRaffleWinnings(); + const { data: userSablierStreams, isFetching: isFetchinguserSablierStreams } = + useUserSablierStreams(); const isPreLaunch = getIsPreLaunch(currentEpoch); - const didUserWinAnyRaffles = !!userRaffleWinnings && userRaffleWinnings.sum > 0; + const didUserWinAnyRaffles = + !!userSablierStreams && + (userSablierStreams.sumAvailable > 0 || + (userSablierStreams.sablierStreams.some(({ isCancelled }) => isCancelled) && + userSablierStreams.sumAvailable > 0)); + + const buttonText = useMemo(() => { + if (userSablierStreams && userSablierStreams.sumAvailable > 0n) { + return t('editLockedGLM'); + } + if (!depositsValue || (!!depositsValue && depositsValue === 0n)) { + return i18n.t('common.lockGlm'); + } + return t('editLockedGLM'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [depositsValue, userSablierStreams?.sumAvailable]); return ( <> @@ -59,12 +74,17 @@ const HomeGridCurrentGlmLock: FC = ({ className }) dataTest="HomeGridCurrentGlmLock--current" isFetching={ isFetchingDepositValue || - isFetchingUserRaffleWinnings || + isFetchinguserSablierStreams || (isAppWaitingForTransactionToBeIndexed && _first(transactionsPending)?.type !== 'withdrawal') } showCryptoSuffix - valueCrypto={(depositsValue || 0n) + (userRaffleWinnings?.sum || 0n)} + valueCrypto={ + (depositsValue || 0n) + + ((!userSablierStreams?.sablierStreams.some(({ isCancelled }) => isCancelled) && + userSablierStreams?.sumAvailable) || + 0n) + } variant={isMobile ? 'large' : 'extra-large'} />
@@ -104,9 +124,7 @@ const HomeGridCurrentGlmLock: FC = ({ className }) onClick={() => setIsModalLockGlmOpen(true)} variant="cta" > - {!depositsValue || (!!depositsValue && depositsValue === 0n) - ? i18n.t('common.lockGlm') - : t('editLockedGLM')} + {buttonText}
diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.module.scss b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.module.scss index aafbfc59d3..975ed2572d 100644 --- a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.module.scss +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.module.scss @@ -36,3 +36,33 @@ } } } + +.timeLockedInSablier { + text-align: left; + position: relative; +} + +.tooltipWrapper { + left: 1rem; + top: -6.8rem; + + .tooltip { + position: initial; + } +} + +.svgWrapper { + width: 1.6rem; + height: 1.6rem; +} + +.tooltipBox { + margin: 0.1rem 0 0 0.4rem; +} + +.unlockInSablierButton { + position: absolute; + padding: 0; + font-size: 1rem; + bottom: -1.4rem; +} diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.tsx index 6d02234591..de4a46cc06 100644 --- a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.tsx @@ -1,12 +1,13 @@ import cx from 'classnames'; -import { format } from 'date-fns'; import React, { FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import BoxRounded from 'components/ui/BoxRounded'; +import Button from 'components/ui/Button'; +import { SABLIER_APP_LINK } from 'constants/urls'; import useAvailableFundsGlm from 'hooks/helpers/useAvailableFundsGlm'; import useDepositValue from 'hooks/queries/useDepositValue'; -import useUserRaffleWinnings from 'hooks/queries/useUserRaffleWinnings'; +import useUserSablierStreams from 'hooks/queries/useUserSablierStreams'; import getFormattedGlmValue from 'utils/getFormattedGlmValue'; import AvailableFundsGlm from './AvailableFundsGlm'; @@ -22,41 +23,57 @@ const LockGlmBudgetBox: FC = ({ const { data: depositsValue, isFetching: isFetchingDepositValue } = useDepositValue(); const { data: availableFundsGlm, isFetching: isFetchingAvailableFundsGlm } = useAvailableFundsGlm(); - const { data: userRaffleWinnings, isFetching: isFetchingUserRaffleWinnings } = - useUserRaffleWinnings(); + const { data: userSablierStreams, isFetching: isFetchinguserSablierStreams } = + useUserSablierStreams(); const { t, i18n } = useTranslation('translation', { keyPrefix: 'components.home.homeGridCurrentGlmLock.modalLockGlm.lockGlmBudgetBox', }); const depositsValueString = useMemo( - () => getFormattedGlmValue({ value: depositsValue || BigInt(0) }).fullString, - [depositsValue], + () => + getFormattedGlmValue({ + value: + (depositsValue || 0n) + + ((currentMode === 'lock' && userSablierStreams?.sumAvailable) || 0n), + }).fullString, + [depositsValue, currentMode, userSablierStreams?.sumAvailable], ); const shouldRaffleWinningsBeDisplayed = - currentMode === 'unlock' && userRaffleWinnings && userRaffleWinnings.sum > 0; - const areFundsFetching = isFetchingAvailableFundsGlm || isFetchingUserRaffleWinnings; + currentMode === 'unlock' && + userSablierStreams && + userSablierStreams.sumAvailable > 0 && + !userSablierStreams.sablierStreams.some(({ isCancelled }) => isCancelled); + const areFundsFetching = isFetchingAvailableFundsGlm || isFetchinguserSablierStreams; const secondRowValue = getFormattedGlmValue({ value: shouldRaffleWinningsBeDisplayed - ? userRaffleWinnings?.sum + ? userSablierStreams?.sumAvailable : BigInt(availableFundsGlm ? availableFundsGlm!.value : 0), }).fullString; const secondRowLabel = useMemo(() => { if (shouldRaffleWinningsBeDisplayed) { - const date = format( - parseInt(userRaffleWinnings?.winnings[0].dateAvailableForWithdrawal, 10) * 1000, - 'd LLL y', + return ( +
+ {userSablierStreams.sum === userSablierStreams.sumAvailable + ? t('timeLockedInSablier') + : t('lockedInSablier')} + +
); - return userRaffleWinnings?.winnings.length > 1 - ? t('raffleWinnings.multipleWins') - : t('raffleWinnings.oneWin', { date }); } return t('walletBalance'); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldRaffleWinningsBeDisplayed, userRaffleWinnings?.winnings.length, i18n.language]); + }, [shouldRaffleWinningsBeDisplayed, userSablierStreams?.sablierStreams.length, i18n.language]); return ( = ({ const { data: availableFundsGlm } = useAvailableFundsGlm(); const { data: depositsValue } = useDepositValue(); - const { data: userRaffleWinnings } = useUserRaffleWinnings(); + const { data: userSablierStreams } = useUserSablierStreams(); const isMaxDisabled = isLoading || step > 1; @@ -86,7 +86,7 @@ const LockGlmTabs: FC = ({ const isButtonDisabled = !formik.isValid || parseUnitsBigInt(formik.values.valueToDeposeOrWithdraw || '0') === 0n; - const didUserWinAnyRaffles = !!userRaffleWinnings && userRaffleWinnings.sum > 0; + const didUserWinAnyRaffles = !!userSablierStreams && userSablierStreams.sum > 0; const shouldRaffleLabelBeVisible = didUserWinAnyRaffles && currentMode === 'unlock'; return ( @@ -142,7 +142,7 @@ const LockGlmTabs: FC = ({ { getFormattedGlmValue({ value: shouldRaffleLabelBeVisible - ? userRaffleWinnings?.sum + ? userSablierStreams?.sum : depositsValue || BigInt(0), }).value } diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/RaffleWinnerBadge/RaffleWinnerBadge.module.scss b/client/src/components/Home/HomeGridCurrentGlmLock/RaffleWinnerBadge/RaffleWinnerBadge.module.scss index 4a8be74725..dd79a0c0a7 100644 --- a/client/src/components/Home/HomeGridCurrentGlmLock/RaffleWinnerBadge/RaffleWinnerBadge.module.scss +++ b/client/src/components/Home/HomeGridCurrentGlmLock/RaffleWinnerBadge/RaffleWinnerBadge.module.scss @@ -15,6 +15,25 @@ $padding: 1.1rem; padding: 0 $padding; transition: opacity $transition-time-1; + &.isSablierStreamCancelled { + background: $color-octant-orange5; + color: $color-octant-orange; + + .tooltipWrapper { + &:hover { + path { + stroke: $color-octant-orange !important; + } + } + } + + .img { + path { + stroke: $color-octant-orange; + } + } + } + .img { margin-right: $padding; } @@ -29,6 +48,8 @@ $padding: 1.1rem; } .tooltipWrapper { + user-select: none; + &:hover { path { stroke: $color-white !important; diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/RaffleWinnerBadge/RaffleWinnerBadge.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/RaffleWinnerBadge/RaffleWinnerBadge.tsx index cd36e9b849..626a2ef6c8 100644 --- a/client/src/components/Home/HomeGridCurrentGlmLock/RaffleWinnerBadge/RaffleWinnerBadge.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/RaffleWinnerBadge/RaffleWinnerBadge.tsx @@ -1,14 +1,14 @@ import cx from 'classnames'; import { format } from 'date-fns'; -import React, { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import Svg from 'components/ui/Svg/Svg'; import Tooltip from 'components/ui/Tooltip'; import useGetValuesToDisplay from 'hooks/helpers/useGetValuesToDisplay'; import useDepositValue from 'hooks/queries/useDepositValue'; -import useUserRaffleWinnings from 'hooks/queries/useUserRaffleWinnings'; -import { gift } from 'svg/misc'; +import useUserSablierStreams from 'hooks/queries/useUserSablierStreams'; +import { cross, gift } from 'svg/misc'; import getFormattedValueWithSymbolSuffix from 'utils/getFormattedValueWithSymbolSuffix'; import { parseUnitsBigInt } from 'utils/parseUnitsBigInt'; @@ -22,30 +22,34 @@ const RaffleWinnerBadge: FC = ({ isVisible }) => { const getValuesToDisplay = useGetValuesToDisplay(); const { data: depositsValue } = useDepositValue(); - const { data: userRaffleWinnings } = useUserRaffleWinnings(); + const { data: userSablierStreams } = useUserSablierStreams(); - const userRaffleWinningsSumFormatted = userRaffleWinnings + const userSablierStreamsSumFormatted = userSablierStreams ? getValuesToDisplay({ cryptoCurrency: 'golem', showFiatPrefix: false, - valueCrypto: userRaffleWinnings.sum, + valueCrypto: userSablierStreams.sumAvailable, }) : undefined; - const userRaffleWinningsSumFloat = userRaffleWinningsSumFormatted - ? parseFloat(userRaffleWinningsSumFormatted.primary.replace(/\s/g, '')) + const isSablierStreamCancelled = userSablierStreams?.sablierStreams.some( + ({ isCancelled }) => isCancelled, + ); + + const userSablierStreamsSumFloat = userSablierStreamsSumFormatted + ? parseFloat(userSablierStreamsSumFormatted.primary.replace(/\s/g, '')) : 0; - const userRaffleWinningsSumFormattedWithSymbolSuffix = getFormattedValueWithSymbolSuffix({ + const userSablierStreamsSumFormattedWithSymbolSuffix = getFormattedValueWithSymbolSuffix({ format: 'thousands', precision: 0, - value: userRaffleWinningsSumFloat, + value: userSablierStreamsSumFloat, }); - const tooltipWinningsText = userRaffleWinnings?.winnings.reduce((acc, curr, index) => { + const tooltipWinningsText = userSablierStreams?.sablierStreams.reduce((acc, curr, index) => { const amountFormatted = getValuesToDisplay({ cryptoCurrency: 'golem', showCryptoSuffix: true, - valueCrypto: parseUnitsBigInt(curr.amount, 'wei'), + valueCrypto: parseUnitsBigInt(curr.remainingAmount, 'wei'), }); const newRow = t('tooltipWinningRow', { date: format(parseInt(curr.dateAvailableForWithdrawal, 10) * 1000, 'd LLL y'), @@ -63,13 +67,30 @@ const RaffleWinnerBadge: FC = ({ isVisible }) => { }) : undefined; - const tooltipText = - depositsValue && depositsValue > 0n && depositsValueFormatted - ? `${tooltipWinningsText}\n${t('tooltipCurrentBalanceRow', { value: depositsValueFormatted.primary })}` - : tooltipWinningsText; + const tooltipText = useMemo(() => { + if (isSablierStreamCancelled) { + return t('tooltipStreamCancelled'); + } + if (depositsValue && depositsValue > 0n && depositsValueFormatted) { + return `${tooltipWinningsText}\n${t('tooltipCurrentBalanceRow', { value: depositsValueFormatted.primary })}`; + } + return tooltipWinningsText; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + depositsValue, + isSablierStreamCancelled, + tooltipWinningsText, + depositsValueFormatted?.primary, + ]); return ( -
+
= ({ isVisible }) => { text={tooltipText} tooltipClassName={styles.tooltip} > - - {t('text', { value: userRaffleWinningsSumFormattedWithSymbolSuffix })} + + {t(isSablierStreamCancelled ? 'textCancelled' : 'text', { + value: userSablierStreamsSumFormattedWithSymbolSuffix, + })}
); diff --git a/client/src/constants/urls.ts b/client/src/constants/urls.ts index dc87ee02d4..66d60eac5e 100644 --- a/client/src/constants/urls.ts +++ b/client/src/constants/urls.ts @@ -16,3 +16,4 @@ export const TIME_OUT_LIST_DISPUTE_FORM = 'https://octant.fillout.com/t/wLNsbSGJ export const SYBIL_ATTACK_EXPLANATION = 'https://chain.link/education-hub/sybil-attack'; export const PRIVACY_POLICY = 'https://docs.octant.app/privacy-policy.html'; export const KARMA_GAP = 'https://gap.karmahq.xyz'; +export const SABLIER_APP_LINK = 'https://app.sablier.com/'; diff --git a/client/src/hooks/queries/useUserRaffleWinnings.ts b/client/src/hooks/queries/useUserRaffleWinnings.ts deleted file mode 100644 index 4c9c3787f3..0000000000 --- a/client/src/hooks/queries/useUserRaffleWinnings.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import { useAccount } from 'wagmi'; - -import { apiGetUserRaffleWinnings, Response } from 'api/calls/userWinnings'; -import { QUERY_KEYS } from 'api/queryKeys'; -import { parseUnitsBigInt } from 'utils/parseUnitsBigInt'; - -type ReturnType = { - sum: bigint; - winnings: Response['winnings']; -}; - -export default function useUserRaffleWinnings(): UseQueryResult { - const { address } = useAccount(); - - return useQuery({ - enabled: !!address, - queryFn: () => apiGetUserRaffleWinnings(address!), - queryKey: QUERY_KEYS.raffleWinnings(address!), - select: response => ({ - sum: response.winnings.reduce( - (acc, curr) => acc + parseUnitsBigInt(curr.amount, 'wei'), - BigInt(0), - ), - winnings: response.winnings, - }), - }); -} diff --git a/client/src/hooks/queries/useUserSablierStreams.ts b/client/src/hooks/queries/useUserSablierStreams.ts new file mode 100644 index 0000000000..16835bcee5 --- /dev/null +++ b/client/src/hooks/queries/useUserSablierStreams.ts @@ -0,0 +1,31 @@ +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { useAccount } from 'wagmi'; + +import { apiGetUserSablierStreams, Response } from 'api/calls/userSablierStreams'; +import { QUERY_KEYS } from 'api/queryKeys'; +import { parseUnitsBigInt } from 'utils/parseUnitsBigInt'; + +type ReturnType = { + sablierStreams: Response['sablierStreams']; + sum: bigint; + sumAvailable: bigint; +}; + +export default function useUserSablierStreams(): UseQueryResult { + const { address } = useAccount(); + + return useQuery({ + enabled: !!address, + queryFn: () => apiGetUserSablierStreams(address!), + queryKey: QUERY_KEYS.sablierStreams(address!), + select: response => ({ + sablierStreams: response.sablierStreams, + sum: response.sablierStreams.reduce((acc, curr) => { + return acc + parseUnitsBigInt(curr.amount, 'wei'); + }, BigInt(0)), + sumAvailable: response.sablierStreams.reduce((acc, curr) => { + return acc + parseUnitsBigInt(curr.remainingAmount, 'wei'); + }, BigInt(0)), + }), + }); +} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 018444dd87..5cf3f5fb87 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -118,15 +118,16 @@ "availableToUnlock": "Available to unlock", "currentlyLocked": "Currently Locked", "walletBalance": "Wallet balance", - "raffleWinnings": { - "oneWin": "Locked until {{date}}", - "multipleWins": "Time locked winnings" - } + "timeLockedInSablier": "Time locked in Sablier", + "lockedInSablier": "Locked in Sablier", + "unlockInSablier": "Unlock in Sablier" } }, "raffleWinnerBadge": { - "text": "{{value}} GLM winnings", - "tooltipWinningRow": "{{value}} prize locked until {{date}}", + "text": "{{value}} GLM", + "textCancelled": "{{value}} GLM stream cancelled", + "tooltipWinningRow": "{{value}} GLM locked in Sablier until {{date}}", + "tooltipStreamCancelled": "Your Sablier stream was canceled due to your inactivity in the last allocation window. If you believe this is a mistake, please open a help ticket on Discord.", "tooltipCurrentBalanceRow": "{{value}} your locked balance" } },