From 42a0675bf2d3c14529556a98b0516f6cd1a00ba1 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski <rafal.czajkowski@keep.network> Date: Wed, 9 Nov 2022 13:38:37 +0100 Subject: [PATCH 1/6] Move fetching owner stakes to redux We want to fetch the owner stakes only once so the best place for fetching this data is one-shot listener effect. --- src/hooks/useFetchOwnerStakes.ts | 163 ------------------ src/store/staking/effects.ts | 38 ++++ src/store/staking/stakingSlice.ts | 13 +- .../staking/__test__/staking.test.ts | 4 +- src/threshold-ts/staking/index.ts | 5 +- 5 files changed, 53 insertions(+), 170 deletions(-) delete mode 100644 src/hooks/useFetchOwnerStakes.ts diff --git a/src/hooks/useFetchOwnerStakes.ts b/src/hooks/useFetchOwnerStakes.ts deleted file mode 100644 index ae463783c..000000000 --- a/src/hooks/useFetchOwnerStakes.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { useCallback } from "react" -import { BigNumber } from "@ethersproject/bignumber" -import { - useTStakingContract, - T_STAKING_CONTRACT_DEPLOYMENT_BLOCK, - usePREContract, - useKeepTokenStakingContract, -} from "../web3/hooks" -import { - getContractPastEvents, - getAddress, - isSameETHAddress, - isAddress, -} from "../web3/utils" -import { StakeType, Token } from "../enums" -import { StakeData } from "../types/staking" -import { setStakes } from "../store/staking" -import { useDispatch } from "react-redux" -import { useFetchPreConfigData } from "./useFetchPreConfigData" -import { useTConvertedAmount } from "./useTConvertedAmount" -import { useNuStakingEscrowContract } from "../web3/hooks/useNuStakingEscrowContract" -import { useThreshold } from "../contexts/ThresholdContext" - -export const useFetchOwnerStakes = () => { - const tStakingContract = useTStakingContract() - - const keepStakingContract = useKeepTokenStakingContract() - const nuStakingEscrowContract = useNuStakingEscrowContract() - - const simplePREApplicationContract = usePREContract() - - const fetchPreConfigData = useFetchPreConfigData() - - const { convertToT: convertKeepToT } = useTConvertedAmount(Token.Keep, "0") - const { convertToT: convertNuToT } = useTConvertedAmount(Token.Nu, "0") - - const dispatch = useDispatch() - const threshold = useThreshold() - - return useCallback( - async (address?: string): Promise<StakeData[]> => { - if ( - !tStakingContract || - !nuStakingEscrowContract || - !keepStakingContract || - !simplePREApplicationContract || - !address - ) { - return [] - } - - const stakedEvents = ( - await getContractPastEvents(tStakingContract, { - eventName: "Staked", - fromBlock: T_STAKING_CONTRACT_DEPLOYMENT_BLOCK, - filterParams: [undefined, address], - }) - ).reverse() - - const stakingProviderToBeneficiary = stakedEvents.reduce( - (reducer, event): { [stakingProvider: string]: string } => { - reducer[event.args?.stakingProvider as string] = event.args - ?.beneficiary as string - return reducer - }, - {} as { [stakingProvider: string]: string } - ) - - const stakingProviders = Object.keys(stakingProviderToBeneficiary) - - const preConfigData = await fetchPreConfigData(stakingProviders) - - const eligibleKeepStakes = await threshold.multicall.aggregate( - stakingProviders.map((stakingProvider) => ({ - interface: keepStakingContract.interface, - address: keepStakingContract.address, - method: "eligibleStake", - args: [stakingProvider, tStakingContract.address], - })) - ) - - // The NU staker can have only one stake. - const { stakingProvider: nuStakingProvider, value: nuStake } = - await nuStakingEscrowContract.stakerInfo(address) - - const stakes = stakedEvents.map((_) => { - const amount = _.args?.amount.toString() - const stakeType = _.args?.stakeType as StakeType - const stakingProvider = getAddress(_.args?.stakingProvider as string) - - return { - stakeType, - owner: _.args?.owner as string, - stakingProvider, - beneficiary: _.args?.beneficiary as string, - authorizer: _.args?.authorizer as string, - blockNumber: _.blockNumber, - blockHash: _.blockHash, - transactionHash: _.transactionHash, - nuInTStake: stakeType === StakeType.NU ? amount : "0", - keepInTStake: stakeType === StakeType.KEEP ? amount : "0", - tStake: stakeType === StakeType.T ? amount : "0", - preConfig: preConfigData[stakingProvider], - } as StakeData - }) - - const data = await threshold.multicall.aggregate( - stakes.map((_) => ({ - interface: tStakingContract.interface, - address: tStakingContract.address, - method: "stakes", - args: [_.stakingProvider], - })) - ) - - data.forEach((_, index) => { - const total = BigNumber.from(_.tStake) - .add(BigNumber.from(_.keepInTStake)) - .add(BigNumber.from(_.nuInTStake)) - const keepInTStake = _.keepInTStake.toString() - const keepEligableStakeInT = convertKeepToT( - eligibleKeepStakes[index].toString() - ) - const possibleKeepTopUpInT = BigNumber.from(keepEligableStakeInT) - .sub(BigNumber.from(keepInTStake)) - .toString() - - const stakingProvider = stakes[index].stakingProvider - const nuInTStake = stakes[index].nuInTStake.toString() - - const possibleNuTopUpInT = - isAddress(nuStakingProvider) && - isSameETHAddress(stakingProvider, nuStakingProvider) - ? BigNumber.from(convertNuToT(nuStake)) - .sub(BigNumber.from(nuInTStake)) - .toString() - : "0" - - stakes[index] = { - ...stakes[index], - tStake: _.tStake.toString(), - keepInTStake, - nuInTStake, - totalInTStake: total.toString(), - possibleKeepTopUpInT, - possibleNuTopUpInT, - } - }) - - dispatch(setStakes(stakes)) - - return stakes - }, - [ - tStakingContract, - dispatch, - convertKeepToT, - convertNuToT, - fetchPreConfigData, - threshold, - ] - ) -} diff --git a/src/store/staking/effects.ts b/src/store/staking/effects.ts index 97dc954ef..0e98270b2 100644 --- a/src/store/staking/effects.ts +++ b/src/store/staking/effects.ts @@ -1,5 +1,7 @@ +import { Stake } from "../../threshold-ts/staking" import { StakeData } from "../../types" import { AddressZero, isAddress, isAddressZero } from "../../web3/utils" +import { walletConnected } from "../account" import { AppListenerEffectAPI } from "../listener" import { selectStakeByStakingProvider } from "./selectors" import { requestStakeByStakingProvider, setStakes } from "./stakingSlice" @@ -82,3 +84,39 @@ const fetchStake = async ( ]) ) } + +export const fetchOwnerStakesEffect = async ( + actionCreator: ReturnType<typeof walletConnected>, + listenerApi: AppListenerEffectAPI +) => { + const address = actionCreator.payload + if (!isAddress(address)) return + + listenerApi.unsubscribe() + + try { + const stakes = await listenerApi.extra.threshold.staking.getOwnerStakes( + address + ) + + listenerApi.dispatch( + setStakes( + stakes.map( + (stake) => + ({ + ...stake, + tStake: stake.tStake.toString(), + nuInTStake: stake.tStake.toString(), + keepInTStake: stake.keepInTStake.toString(), + totalInTStake: stake.totalInTStake.toString(), + possibleKeepTopUpInT: stake.possibleKeepTopUpInT.toString(), + possibleNuTopUpInT: stake.possibleNuTopUpInT.toString(), + } as Stake<string>) + ) + ) + ) + } catch (error) { + console.log("Could not fetch owner stakes", error) + listenerApi.subscribe() + } +} diff --git a/src/store/staking/stakingSlice.ts b/src/store/staking/stakingSlice.ts index c2d7a006a..4923180e5 100644 --- a/src/store/staking/stakingSlice.ts +++ b/src/store/staking/stakingSlice.ts @@ -12,7 +12,11 @@ import { StakeType, TopUpType, UnstakeType } from "../../enums" import { AddressZero } from "../../web3/utils" import { UpdateStateActionPayload } from "../../types/state" import { startAppListening } from "../listener" -import { fetchStakeByStakingProviderEffect } from "./effects" +import { walletConnected } from "../account" +import { + fetchStakeByStakingProviderEffect, + fetchOwnerStakesEffect, +} from "./effects" interface StakingState { stakingProvider: string @@ -187,10 +191,13 @@ export const { } = stakingSlice.actions export const registerStakingListeners = () => { + startAppListening({ + actionCreator: walletConnected, + effect: fetchOwnerStakesEffect, + }) + startAppListening({ actionCreator: requestStakeByStakingProvider, effect: fetchStakeByStakingProviderEffect, }) } - -registerStakingListeners() diff --git a/src/threshold-ts/staking/__test__/staking.test.ts b/src/threshold-ts/staking/__test__/staking.test.ts index 310410b2c..b46f69dac 100644 --- a/src/threshold-ts/staking/__test__/staking.test.ts +++ b/src/threshold-ts/staking/__test__/staking.test.ts @@ -218,10 +218,10 @@ describe("Staking test", () => { ]) expect(vendingMachines.keep.convertToT).toHaveBeenCalledWith( - eligibleKeepStake + eligibleKeepStake.toString() ) expect(vendingMachines.nu.convertToT).toHaveBeenCalledWith( - nuStakerInfo.value + nuStakerInfo.value.toString() ) expect(result).toEqual({ diff --git a/src/threshold-ts/staking/index.ts b/src/threshold-ts/staking/index.ts index 5c3d5c640..709487fcd 100644 --- a/src/threshold-ts/staking/index.ts +++ b/src/threshold-ts/staking/index.ts @@ -199,12 +199,13 @@ export class Staking implements IStaking { isAddress(nuStakingProvider) && isSameETHAddress(stakingProvider, nuStakingProvider) ? BigNumber.from( - (await this._vendingMachines.nu.convertToT(nuStake)).tAmount + (await this._vendingMachines.nu.convertToT(nuStake.toString())) + .tAmount ).sub(BigNumber.from(nuInTStake)) : ZERO const keepEligableStakeInT = ( - await this._vendingMachines.keep.convertToT(eligibleKeepStake) + await this._vendingMachines.keep.convertToT(eligibleKeepStake.toString()) ).tAmount const possibleKeepTopUpInT = BigNumber.from(keepEligableStakeInT).sub( BigNumber.from(keepInTStake) From fced6c386ffb59e81b2afb53159f8bdfbbf9b8a5 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski <rafal.czajkowski@keep.network> Date: Wed, 9 Nov 2022 13:42:19 +0100 Subject: [PATCH 2/6] Refactor registering listeners We should register listener in different file than the one in which it is defined otherwise we may run into an issue with webpack import orddering. Here we create a common function that should register all listeners and we call this fn after creating a store and after resetting the store. --- src/store/account/slice.ts | 1 - src/store/index.ts | 20 ++++++++------------ src/store/listener.ts | 11 +++++++++++ src/store/staking-applications/slice.ts | 1 - src/store/tokens/tokenSlice.ts | 1 - 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/store/account/slice.ts b/src/store/account/slice.ts index 570f397f8..75a7c4252 100644 --- a/src/store/account/slice.ts +++ b/src/store/account/slice.ts @@ -103,7 +103,6 @@ export const registerAccountListeners = () => { }) } } -registerAccountListeners() export const { walletConnected, diff --git a/src/store/index.ts b/src/store/index.ts index 242ea4aec..cf2175dcd 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -5,19 +5,16 @@ import { Reducer, } from "@reduxjs/toolkit" import { modalSlice } from "./modal" -import { registerTokensListeners, tokenSlice } from "./tokens" +import { tokenSlice } from "./tokens" import { sidebarSlice } from "./sidebar" import { transactionSlice } from "./transactions" -import { registerStakingListeners, stakingSlice } from "./staking" +import { stakingSlice } from "./staking" import { ethSlice } from "./eth" import { rewardsSlice } from "./rewards" import { tbtcSlice } from "./tbtc" -import { - registerStakingAppsListeners, - stakingApplicationsSlice, -} from "./staking-applications/slice" -import { listenerMiddleware } from "./listener" -import { accountSlice, registerAccountListeners } from "./account" +import { stakingApplicationsSlice } from "./staking-applications/slice" +import { listenerMiddleware, registerListeners } from "./listener" +import { accountSlice } from "./account" const combinedReducer = combineReducers({ account: accountSlice.reducer, @@ -41,10 +38,7 @@ export const resetStoreAction = () => ({ const rootReducer: Reducer = (state: RootState, action: AnyAction) => { if (action.type === APP_RESET_STORE) { listenerMiddleware.clearListeners() - registerStakingListeners() - registerStakingAppsListeners() - registerAccountListeners() - registerTokensListeners() + registerListeners() state = { eth: { ...state.eth }, token: { @@ -93,6 +87,8 @@ const store = configureStore({ }).prepend(listenerMiddleware.middleware), }) +registerListeners() + export type RootState = ReturnType< typeof store.getState & typeof combinedReducer > diff --git a/src/store/listener.ts b/src/store/listener.ts index f4f65b845..f760f19bf 100644 --- a/src/store/listener.ts +++ b/src/store/listener.ts @@ -6,6 +6,10 @@ import { import { AppDispatch, RootState } from "." import { Threshold } from "../threshold-ts" import { threshold } from "../utils/getThresholdLib" +import { registerTokensListeners } from "./tokens" +import { registerStakingListeners } from "./staking" +import { registerStakingAppsListeners } from "./staking-applications/slice" +import { registerAccountListeners } from "./account" export const listenerMiddleware = createListenerMiddleware({ extra: { threshold }, @@ -29,3 +33,10 @@ export type AppListenerEffectAPI = ListenerEffectAPI< export const startAppListening = listenerMiddleware.startListening as AppStartListening + +export const registerListeners = () => { + registerAccountListeners() + registerTokensListeners() + registerStakingListeners() + registerStakingAppsListeners() +} diff --git a/src/store/staking-applications/slice.ts b/src/store/staking-applications/slice.ts index 4fdeb67d1..bcc030374 100644 --- a/src/store/staking-applications/slice.ts +++ b/src/store/staking-applications/slice.ts @@ -282,4 +282,3 @@ export const registerStakingAppsListeners = () => { }) } } -registerStakingAppsListeners() diff --git a/src/store/tokens/tokenSlice.ts b/src/store/tokens/tokenSlice.ts index 7f1f5a515..847d74d60 100644 --- a/src/store/tokens/tokenSlice.ts +++ b/src/store/tokens/tokenSlice.ts @@ -114,4 +114,3 @@ export const registerTokensListeners = () => { effect: fetchTokenBalances, }) } -registerTokensListeners() From 17d6e1ba752e3b5ee3ee563592662e8fc7fe120e Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski <rafal.czajkowski@keep.network> Date: Wed, 9 Nov 2022 14:41:17 +0100 Subject: [PATCH 3/6] Clean up the `StakeData` type Use the `Stake` type from the threshold ts lib- this removes the `preConfig` filed from the `StakeData` type. The PRE is a staking app so should be stored in a dedicated slice(rewards). We are going to add the PRE app to the rewards slice in a follow-up work. --- .../Modal/TopupTModal/LegacyTopUpModal.tsx | 2 +- src/hooks/useSubscribeToStakedEvent.ts | 17 ++------------ src/pages/Staking/StakeCard/index.tsx | 6 ++--- src/store/staking/stakingSlice.ts | 6 ----- src/types/staking.ts | 22 +++---------------- 5 files changed, 9 insertions(+), 44 deletions(-) diff --git a/src/components/Modal/TopupTModal/LegacyTopUpModal.tsx b/src/components/Modal/TopupTModal/LegacyTopUpModal.tsx index 44801372b..f836ceeb0 100644 --- a/src/components/Modal/TopupTModal/LegacyTopUpModal.tsx +++ b/src/components/Modal/TopupTModal/LegacyTopUpModal.tsx @@ -108,7 +108,7 @@ const LegacyTopUpModal: FC<BaseModalProps & { stake: StakeData }> = ({ <Button as={Link} isExternal - href={stakeTypeToDappHref[stake.stakeType]} + href={stakeTypeToDappHref[stake.stakeType ?? StakeType.KEEP]} isFullWidth mb="3" > diff --git a/src/hooks/useSubscribeToStakedEvent.ts b/src/hooks/useSubscribeToStakedEvent.ts index 4aee43123..96d708e59 100644 --- a/src/hooks/useSubscribeToStakedEvent.ts +++ b/src/hooks/useSubscribeToStakedEvent.ts @@ -7,7 +7,6 @@ import { providerStakedForStakingProvider, } from "../store/staking" import { useSubscribeToContractEvent, useTStakingContract } from "../web3/hooks" -import { isAddress, isSameETHAddress } from "../web3/utils" export const useSubscribeToStakedEvent = () => { const tStakingContract = useTStakingContract() @@ -31,11 +30,8 @@ export const useSubscribeToStakedEvent = () => { stakingProvider: string, beneficiary: string, authorizer: string, - amount: BigNumberish, - event: Event + amount: BigNumberish ) => { - // TODO: open success modal here - const { blockNumber, blockHash, transactionHash } = event dispatch( providerStaked({ stakeType, @@ -43,9 +39,6 @@ export const useSubscribeToStakedEvent = () => { stakingProvider, authorizer, beneficiary, - blockHash, - blockNumber, - transactionHash, amount: amount.toString(), }) ) @@ -70,11 +63,8 @@ export const useSubscribeToStakedEvent = () => { stakingProvider: string, beneficiary: string, authorizer: string, - amount: BigNumberish, - event: Event + amount: BigNumberish ) => { - // TODO: open success modal here - const { blockNumber, blockHash, transactionHash } = event dispatch( providerStakedForStakingProvider({ stakeType, @@ -82,9 +72,6 @@ export const useSubscribeToStakedEvent = () => { stakingProvider, authorizer, beneficiary, - blockHash, - blockNumber, - transactionHash, amount: amount.toString(), }) ) diff --git a/src/pages/Staking/StakeCard/index.tsx b/src/pages/Staking/StakeCard/index.tsx index 729a59880..2dacb3ba2 100644 --- a/src/pages/Staking/StakeCard/index.tsx +++ b/src/pages/Staking/StakeCard/index.tsx @@ -41,9 +41,9 @@ const StakeCardProvider: FC<{ stake: StakeData }> = ({ stake }) => { const canTopUpKepp = BigNumber.from(stake.possibleKeepTopUpInT).gt(0) const canTopUpNu = BigNumber.from(stake.possibleNuTopUpInT).gt(0) const hasLegacyStakes = stake.nuInTStake !== "0" || stake.keepInTStake !== "0" - const isPRESet = - !isAddressZero(stake.preConfig.operator) && - stake.preConfig.isOperatorConfirmed + const isPRESet = true + // !isAddressZero(stake.preConfig.operator) && + // stake.preConfig.isOperatorConfirmed return ( <StakeCardContext.Provider diff --git a/src/store/staking/stakingSlice.ts b/src/store/staking/stakingSlice.ts index 4923180e5..e17af29bc 100644 --- a/src/store/staking/stakingSlice.ts +++ b/src/store/staking/stakingSlice.ts @@ -75,12 +75,6 @@ export const stakingSlice = createSlice({ newStake.possibleKeepTopUpInT = "0" newStake.possibleNuTopUpInT = "0" - newStake.preConfig = { - operator: AddressZero, - isOperatorConfirmed: false, - operatorStartTimestamp: "0", - } - state.stakes = [newStake, ...state.stakes] state.stakedBalance = calculateStakedBalance(state.stakes) }, diff --git a/src/types/staking.ts b/src/types/staking.ts index 86fc84606..7bee481ea 100644 --- a/src/types/staking.ts +++ b/src/types/staking.ts @@ -1,5 +1,6 @@ import { BigNumberish } from "@ethersproject/bignumber" -import { StakeType, TopUpType, UnstakeType } from "../enums" +import { TopUpType, UnstakeType } from "../enums" +import { Stake } from "../threshold-ts/staking" import { UpdateStateActionPayload } from "./state" export type StakingStateKey = @@ -35,23 +36,7 @@ export interface PreConfigData { [stakingProvider: string]: PreConfig } -export interface StakeData { - stakeType: StakeType - owner: string - stakingProvider: string - beneficiary: string - authorizer: string - blockNumber: number - blockHash: string - transactionHash: string - nuInTStake: string - keepInTStake: string - tStake: string - totalInTStake: string - preConfig: PreConfig - possibleKeepTopUpInT: string - possibleNuTopUpInT: string -} +export type StakeData = Stake<string> export interface ProviderStakedEvent { stakeType: number @@ -71,7 +56,6 @@ export type ProviderStakedActionPayload = ProviderStakedEvent & | "tStake" | "amount" | "totalInTStake" - | "preConfig" | "possibleKeepTopUpInT" | "possibleNuTopUpInT" > From 527d535e37c32edda6586125e848d87e4caf44c1 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski <rafal.czajkowski@keep.network> Date: Thu, 10 Nov 2022 14:53:31 +0100 Subject: [PATCH 4/6] Move fetching the PRE app data to ts-lib and redux The PRE is a seprate app so should be stored in the `staking-apps` slice not as a part of the stake data. --- src/hooks/useFetchPreConfigData.ts | 43 ------- src/pages/Staking/StakeCard/index.tsx | 10 +- src/store/staking-applications/effects.ts | 112 ++++++++++++++---- src/store/staking-applications/selectors.ts | 10 ++ src/store/staking-applications/slice.ts | 39 ++++-- .../applications/pre/abi.json} | 0 src/threshold-ts/applications/pre/index.ts | 101 ++++++++++++++++ src/threshold-ts/mas/index.ts | 3 + src/web3/hooks/usePREContract.ts | 24 +--- 9 files changed, 242 insertions(+), 100 deletions(-) delete mode 100644 src/hooks/useFetchPreConfigData.ts rename src/{web3/abi/SimplePreApplication.json => threshold-ts/applications/pre/abi.json} (100%) create mode 100644 src/threshold-ts/applications/pre/index.ts diff --git a/src/hooks/useFetchPreConfigData.ts b/src/hooks/useFetchPreConfigData.ts deleted file mode 100644 index dd0fffdc9..000000000 --- a/src/hooks/useFetchPreConfigData.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useCallback } from "react" -import { usePREContract } from "../web3/hooks" -import { PreConfigData } from "../types/staking" -import { useThreshold } from "../contexts/ThresholdContext" - -export const useFetchPreConfigData = (): (( - stakingProviders: string[] -) => Promise<PreConfigData>) => { - const preContract = usePREContract() - const threshold = useThreshold() - - return useCallback( - async (stakingProviders) => { - if (!stakingProviders || stakingProviders.length === 0 || !preContract) { - return {} as PreConfigData - } - - const preConfigDataRaw = await threshold.multicall.aggregate( - stakingProviders.map((stakingProvider) => { - return { - interface: preContract.interface, - address: preContract.address, - method: "stakingProviderInfo", - args: [stakingProvider], - } - }) - ) - - return preConfigDataRaw.reduce( - (finalData: PreConfigData, _, idx): PreConfigData => { - finalData[stakingProviders[idx]] = { - operator: _.operator, - isOperatorConfirmed: _.operatorConfirmed, - operatorStartTimestamp: _.operatorStartTimestamp.toString(), - } - return finalData - }, - {} - ) - }, - [preContract, threshold] - ) -} diff --git a/src/pages/Staking/StakeCard/index.tsx b/src/pages/Staking/StakeCard/index.tsx index 2dacb3ba2..10a08e766 100644 --- a/src/pages/Staking/StakeCard/index.tsx +++ b/src/pages/Staking/StakeCard/index.tsx @@ -24,7 +24,7 @@ import { TopUpType, UnstakeType, } from "../../../enums" -import { AddressZero, isAddressZero } from "../../../web3/utils" +import { AddressZero } from "../../../web3/utils" import StakeApplications from "./StakeApplications" import StakeCardHeader from "./Header" import StakeRewards from "./StakeRewards" @@ -35,15 +35,17 @@ import { StakeCardContext } from "../../../contexts/StakeCardContext" import { useStakeCardContext } from "../../../hooks/useStakeCardContext" import { isSameETHAddress } from "../../../threshold-ts/utils" import { useWeb3React } from "@web3-react/core" +import { useAppSelector } from "../../../hooks/store" +import { selectPREAppDataByStakingProvider } from "../../../store/staking-applications" const StakeCardProvider: FC<{ stake: StakeData }> = ({ stake }) => { const isInactiveStake = BigNumber.from(stake.totalInTStake).isZero() const canTopUpKepp = BigNumber.from(stake.possibleKeepTopUpInT).gt(0) const canTopUpNu = BigNumber.from(stake.possibleNuTopUpInT).gt(0) const hasLegacyStakes = stake.nuInTStake !== "0" || stake.keepInTStake !== "0" - const isPRESet = true - // !isAddressZero(stake.preConfig.operator) && - // stake.preConfig.isOperatorConfirmed + const { isOperatorMapped: isPRESet } = useAppSelector((state) => + selectPREAppDataByStakingProvider(state, stake.stakingProvider) + ) return ( <StakeCardContext.Provider diff --git a/src/store/staking-applications/effects.ts b/src/store/staking-applications/effects.ts index 86ba37261..cd1907184 100644 --- a/src/store/staking-applications/effects.ts +++ b/src/store/staking-applications/effects.ts @@ -1,5 +1,10 @@ import { AnyAction } from "@reduxjs/toolkit" -import { stakingApplicationsSlice, StakingAppName } from "./slice" +import { + AllStakinApps, + AuthorizationNotRequiredApps, + stakingApplicationsSlice, + StakingAppName, +} from "./slice" import { AppListenerEffectAPI } from "../listener" import { selectStakeByStakingProvider, @@ -22,6 +27,11 @@ import { selectStakingAppStateByAppName, } from "./selectors" import { isAddressZero } from "../../web3/utils" +import { + IPRE, + StakingProviderInfo as PREStakingProviderInfo, +} from "../../threshold-ts/applications/pre" +import { BigNumber, BigNumberish } from "ethers" export const getSupportedAppsEffect = async ( action: ReturnType<typeof stakingApplicationsSlice.actions.getSupportedApps>, @@ -115,17 +125,26 @@ export const getSupportedAppsStakingProvidersData = async ( "randomBeacon", listenerApi ) + + await getPREAppStakingProvidersData( + stakingProviders, + listenerApi.extra.threshold.multiAppStaking.pre, + "pre", + listenerApi + ) } catch (error) { console.log("Could not fetch apps data for staking providers ", error) listenerApi.subscribe() } } -const getKeepStakingAppStakingProvidersData = async ( +const getStakingAppStakingProivdersData = async <DataType, MappedDataType>( stakingProviders: string[], - application: IApplication, - appName: StakingAppName, - listenerApi: AppListenerEffectAPI + application: IPRE | IApplication, + appName: AllStakinApps, + listenerApi: AppListenerEffectAPI, + mapResultTo: (data: DataType) => MappedDataType, + dispatchFn: (data: { [stakingProvider: string]: MappedDataType }) => AnyAction ) => { try { listenerApi.dispatch( @@ -133,32 +152,27 @@ const getKeepStakingAppStakingProvidersData = async ( appName, }) ) - const appData = await Promise.all( - stakingProviders.map(application.getStakingProviderAppInfo) + + const appData: DataType[] = await Promise.all( + stakingProviders.map( + (stakingProvider) => + application.getStakingProviderAppInfo( + stakingProvider + ) as unknown as Promise<DataType> + ) ) + const appDataByStakingProvider = stakingProviders.reduce( (reducer, stakingProvider, index) => { const _appData = appData[index] - reducer[stakingProvider] = { - authorizedStake: _appData.authorizedStake.toString(), - pendingAuthorizationDecrease: - _appData.pendingAuthorizationDecrease.toString(), - remainingAuthorizationDecreaseDelay: - _appData.remainingAuthorizationDecreaseDelay.toString(), - isDeauthorizationReqestActive: _appData.isDeauthorizationReqestActive, - deauthorizationCreatedAt: - _appData.deauthorizationCreatedAt?.toString(), - } + reducer[stakingProvider] = mapResultTo(_appData) + return reducer }, - {} as { [stakingProvider: string]: StakingProviderAppInfo<string> } - ) - listenerApi.dispatch( - stakingApplicationsSlice.actions.setStakingProvidersAppData({ - appName, - data: appDataByStakingProvider, - }) + {} as { [stakingProvider: string]: MappedDataType } ) + + listenerApi.dispatch(dispatchFn(appDataByStakingProvider)) } catch (error) { listenerApi.dispatch( stakingApplicationsSlice.actions.setStakingProvidersAppDataError({ @@ -170,6 +184,56 @@ const getKeepStakingAppStakingProvidersData = async ( } } +const getKeepStakingAppStakingProvidersData = async ( + stakingProviders: string[], + application: IApplication, + appName: StakingAppName, + listenerApi: AppListenerEffectAPI +) => { + type MappedDataType = StakingProviderAppInfo<string> + const mapToResult = (data: StakingProviderAppInfo): MappedDataType => { + return { + authorizedStake: data.authorizedStake.toString(), + pendingAuthorizationDecrease: + data.pendingAuthorizationDecrease.toString(), + remainingAuthorizationDecreaseDelay: + data.remainingAuthorizationDecreaseDelay.toString(), + isDeauthorizationReqestActive: data.isDeauthorizationReqestActive, + deauthorizationCreatedAt: data.deauthorizationCreatedAt?.toString(), + } + } + + await getStakingAppStakingProivdersData< + StakingProviderAppInfo, + MappedDataType + >(stakingProviders, application, appName, listenerApi, mapToResult, (data) => + stakingApplicationsSlice.actions.setStakingProvidersAppData({ + appName, + data, + }) + ) +} + +const getPREAppStakingProvidersData = async ( + stakingProviders: string[], + application: IPRE, + appName: AuthorizationNotRequiredApps, + listenerApi: AppListenerEffectAPI +) => { + type MappedDataType = PREStakingProviderInfo + const mapToResult = (data: PREStakingProviderInfo): MappedDataType => data + + await getStakingAppStakingProivdersData< + PREStakingProviderInfo, + MappedDataType + >(stakingProviders, application, appName, listenerApi, mapToResult, (data) => + stakingApplicationsSlice.actions.setStakingProvidersAppData({ + appName, + data, + }) + ) +} + export const displayMapOperatorToStakingProviderModalEffect = async ( action: AnyAction, listenerApi: AppListenerEffectAPI diff --git a/src/store/staking-applications/selectors.ts b/src/store/staking-applications/selectors.ts index 45d4e2041..f60d0bd1f 100644 --- a/src/store/staking-applications/selectors.ts +++ b/src/store/staking-applications/selectors.ts @@ -52,3 +52,13 @@ export const selectStakingAppByStakingProvider = createSelector( } } ) + +export const selectPREAppDataByStakingProvider = createSelector( + [ + selectStakingAppState, + (_: RootState, stakingProvider: string) => stakingProvider, + ], + (applicationState: StakingApplicationsState, stakingProvider: string) => { + return applicationState.pre.stakingProviders.data[stakingProvider] ?? {} + } +) diff --git a/src/store/staking-applications/slice.ts b/src/store/staking-applications/slice.ts index bcc030374..946b7ea0a 100644 --- a/src/store/staking-applications/slice.ts +++ b/src/store/staking-applications/slice.ts @@ -5,6 +5,7 @@ import { StakingProviderAppInfo, AuthorizationParameters, } from "../../threshold-ts/applications" +import { StakingProviderInfo as PREStakingProviderInfo } from "../../threshold-ts/applications/pre" import { MAX_UINT64 } from "../../threshold-ts/utils" import { FetchingState } from "../../types" import { startAppListening } from "../listener" @@ -24,6 +25,17 @@ type StakingApplicationDataByStakingProvider = { [stakingProvider: string]: StakingProviderAppInfo<string> } +type PREApplicationDataByStakingProvider = { + [stakingProvider: string]: PREStakingProviderInfo +} + +type SetStakingProvidersAppDataPayload = PayloadAction<{ + appName: AllStakinApps + data: + | StakingApplicationDataByStakingProvider + | PREApplicationDataByStakingProvider +}> + export type StakingApplicationState = { parameters: FetchingState<AuthorizationParameters<string>> stakingProviders: FetchingState<StakingApplicationDataByStakingProvider> @@ -32,9 +44,12 @@ export type StakingApplicationState = { export interface StakingApplicationsState { tbtc: StakingApplicationState randomBeacon: StakingApplicationState + pre: { stakingProviders: FetchingState<PREApplicationDataByStakingProvider> } } export type StakingAppName = "tbtc" | "randomBeacon" +export type AuthorizationNotRequiredApps = "pre" +export type AllStakinApps = StakingAppName | AuthorizationNotRequiredApps export const stakingApplicationsSlice = createSlice({ name: "staking-applications", @@ -71,6 +86,13 @@ export const stakingApplicationsSlice = createSlice({ data: {}, }, }, + pre: { + stakingProviders: { + isFetching: false, + error: "", + data: {}, + }, + }, } as StakingApplicationsState, reducers: { getSupportedApps: (state: StakingApplicationsState, action) => {}, @@ -105,12 +127,15 @@ export const stakingApplicationsSlice = createSlice({ }, setStakingProvidersAppData: ( state: StakingApplicationsState, - action: PayloadAction<{ - appName: StakingAppName - data: StakingApplicationDataByStakingProvider - }> + action: SetStakingProvidersAppDataPayload ) => { - const { appName, data } = action.payload + const { appName, data } = action.payload as { + appName: AllStakinApps + data: typeof action.payload.appName extends AuthorizationNotRequiredApps + ? PREApplicationDataByStakingProvider + : StakingApplicationDataByStakingProvider + } + state[appName].stakingProviders = { isFetching: false, error: "", @@ -120,7 +145,7 @@ export const stakingApplicationsSlice = createSlice({ fetchingStakingProvidersAppData: ( state: StakingApplicationsState, action: PayloadAction<{ - appName: StakingAppName + appName: AllStakinApps }> ) => { const { appName } = action.payload @@ -129,7 +154,7 @@ export const stakingApplicationsSlice = createSlice({ setStakingProvidersAppDataError: ( state: StakingApplicationsState, action: PayloadAction<{ - appName: StakingAppName + appName: AllStakinApps error: string }> ) => { diff --git a/src/web3/abi/SimplePreApplication.json b/src/threshold-ts/applications/pre/abi.json similarity index 100% rename from src/web3/abi/SimplePreApplication.json rename to src/threshold-ts/applications/pre/abi.json diff --git a/src/threshold-ts/applications/pre/index.ts b/src/threshold-ts/applications/pre/index.ts new file mode 100644 index 000000000..90c2a72d2 --- /dev/null +++ b/src/threshold-ts/applications/pre/index.ts @@ -0,0 +1,101 @@ +import { BigNumber, Contract } from "ethers" +import SimplePREApplicationABI from "./abi.json" +import { AddressZero, isAddressZero, getContract } from "../../utils" +import { EthereumConfig } from "../../types" + +const PRE_ADDRESSESS = { + // https://etherscan.io/address/0x7E01c9c03FD3737294dbD7630a34845B0F70E5Dd + 1: "0x7E01c9c03FD3737294dbD7630a34845B0F70E5Dd", + // https://goerli.etherscan.io/address/0x829fdCDf6Be747FEA37518fBd83dF70EE371fCf2 + // As NuCypher hasn't depoyed the `SimplePreApplication` contract on Goerli, + // we're using a stub contract. + 5: "0x829fdCDf6Be747FEA37518fBd83dF70EE371fCf2", + // Set the correct `SimplePREApplication` contract address. If you deployed + // the `@threshold-network/solidity-contracts` to your local chain and linked + // package using `yarn link @threshold-network/solidity-contracts` you can + // find the contract address at + // `node_modules/@threshold-network/solidity-contracts/artifacts/SimplePREApplication.json`. + 1337: AddressZero, +} as Record<string, string> + +export interface StakingProviderInfo { + /** + * Operator address mapped to a given staking provider/ + */ + operator: string + /** + * Determines if the operator is confirmed. + */ + isOperatorConfirmed: boolean + /** + * Timestamp where operator were bonded. + */ + operatorStartTimestamp: string + /** + * Determines if the operator for the given staking provider is + * mapped. + */ + isOperatorMapped: boolean +} + +// NOTE: The simple PRE application contract doesn't implement the application +// interface so we can't use the same interface as for the Keep staking apps. +export interface IPRE { + /** + * Application address. + */ + address: string + + /** + * Application contract. + */ + contract: Contract + + getStakingProviderAppInfo: ( + stakingProvider: string + ) => Promise<StakingProviderInfo> +} + +export class PRE implements IPRE { + private _application: Contract + + constructor(config: EthereumConfig) { + const address = PRE_ADDRESSESS[config.chainId] + if (!address) { + throw new Error("Unsupported chain id") + } + + this._application = getContract( + address, + SimplePREApplicationABI, + config.providerOrSigner, + config.account + ) + } + getStakingProviderAppInfo = async ( + stakingProvider: string + ): Promise<StakingProviderInfo> => { + const operatorInfo = (await this._application.stakingProviderInfo( + stakingProvider + )) as { + operator: string + operatorConfirmed: boolean + operatorStartTimestamp: BigNumber + } + + return { + operator: operatorInfo.operator, + isOperatorConfirmed: operatorInfo.operatorConfirmed, + operatorStartTimestamp: operatorInfo.operatorStartTimestamp.toString(), + isOperatorMapped: + !isAddressZero(operatorInfo.operator) && operatorInfo.operatorConfirmed, + } + } + + get address() { + return this._application.address + } + get contract() { + return this._application + } +} diff --git a/src/threshold-ts/mas/index.ts b/src/threshold-ts/mas/index.ts index f63a84e9c..aa30de8eb 100644 --- a/src/threshold-ts/mas/index.ts +++ b/src/threshold-ts/mas/index.ts @@ -8,6 +8,7 @@ import { import { IMulticall, ContractCall } from "../multicall" import { IStaking } from "../staking" import { EthereumConfig } from "../types" +import { IPRE, PRE } from "../applications/pre" export interface SupportedAppAuthorizationParameters { tbtc: AuthorizationParameters @@ -24,6 +25,7 @@ export class MultiAppStaking { private _multicall: IMulticall public readonly randomBeacon: IApplication public readonly ecdsa: IApplication + public readonly pre: IPRE constructor( staking: IStaking, @@ -42,6 +44,7 @@ export class MultiAppStaking { abi: WalletRegistry.abi, ...config, }) + this.pre = new PRE(config) } async getSupportedAppsAuthParameters(): Promise<SupportedAppAuthorizationParameters> { diff --git a/src/web3/hooks/usePREContract.ts b/src/web3/hooks/usePREContract.ts index 16de6d4c0..71682be75 100644 --- a/src/web3/hooks/usePREContract.ts +++ b/src/web3/hooks/usePREContract.ts @@ -1,28 +1,8 @@ -import SimplePREApplicationABI from "../abi/SimplePreApplication.json" -import { useContract } from "./useContract" +import { useThreshold } from "../../contexts/ThresholdContext" import { supportedChainId } from "../../utils/getEnvVariable" -import { ChainID } from "../../enums" -import { AddressZero } from "../utils" export const PRE_DEPLOYMENT_BLOCK = supportedChainId === "1" ? 14141140 : 0 -const PRE_ADDRESSESS = { - // https://etherscan.io/address/0x7E01c9c03FD3737294dbD7630a34845B0F70E5Dd - [ChainID.Ethereum.valueOf().toString()]: - "0x7E01c9c03FD3737294dbD7630a34845B0F70E5Dd", - // https://goerli.etherscan.io/address/0x829fdCDf6Be747FEA37518fBd83dF70EE371fCf2 - // As NuCypher hasn't depoyed the `SimplePreApplication` contract on Goerli, - // we're using a stub contract. - [ChainID.Goerli.valueOf().toString()]: - "0x829fdCDf6Be747FEA37518fBd83dF70EE371fCf2", - // Set the correct `SimplePREApplication` contract address. If you deployed - // the `@threshold-network/solidity-contracts` to your local chain and linked - // package using `yarn link @threshold-network/solidity-contracts` you can - // find the contract address at - // `node_modules/@threshold-network/solidity-contracts/artifacts/SimplePREApplication.json`. - [ChainID.Localhost.valueOf().toString()]: AddressZero, -} as Record<string, string> - export const usePREContract = () => { - return useContract(PRE_ADDRESSESS[supportedChainId], SimplePREApplicationABI) + return useThreshold().multiAppStaking.pre.contract } From 106d739fdc3da39220b006a019fd488c3bc29a1e Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski <rafal.czajkowski@keep.network> Date: Thu, 10 Nov 2022 15:42:58 +0100 Subject: [PATCH 5/6] Add unit tests for PRE service --- .../applications/__tests__/pre.test.ts | 84 +++++++++++++++++++ src/threshold-ts/applications/pre/index.ts | 2 +- 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/threshold-ts/applications/__tests__/pre.test.ts diff --git a/src/threshold-ts/applications/__tests__/pre.test.ts b/src/threshold-ts/applications/__tests__/pre.test.ts new file mode 100644 index 000000000..497f4bca9 --- /dev/null +++ b/src/threshold-ts/applications/__tests__/pre.test.ts @@ -0,0 +1,84 @@ +import { BigNumber, ethers } from "ethers" +import { EthereumConfig } from "../../types" +import { IPRE, PRE, PRE_ADDRESSESS } from "../pre" +import SimplePREApplicationABI from "../pre/abi.json" +import { AddressZero, getContract, isAddressZero } from "../../utils" + +jest.mock("../../utils", () => ({ + ...(jest.requireActual("../../utils") as {}), + getContract: jest.fn(), +})) + +describe("PRE application wrapper test", () => { + let pre: IPRE + let config: EthereumConfig + + const account = "0xaC1933A3Ee78A26E16030801273fBa250631eD5f" + + let mockPREContract: { + address: string + stakingProviderInfo: jest.MockedFn<any> + } + + beforeEach(() => { + config = { + providerOrSigner: {} as ethers.providers.Provider, + chainId: 1, + account, + } + mockPREContract = { + address: PRE_ADDRESSESS[config.chainId], + stakingProviderInfo: jest.fn(), + } + ;(getContract as jest.Mock).mockImplementation(() => mockPREContract) + pre = new PRE(config) + }) + + test("should create the PRE instance correctly", () => { + expect(getContract).toHaveBeenCalledWith( + PRE_ADDRESSESS[config.chainId], + SimplePREApplicationABI, + config.providerOrSigner, + config.account + ) + expect(pre).toBeInstanceOf(PRE) + expect(pre.getStakingProviderAppInfo).toBeDefined() + expect(pre.contract).toBe(mockPREContract) + expect(pre.address).toBe(mockPREContract.address) + }) + + test("should throw an error if pass unsupported chain id to constructor", () => { + expect(() => { + new PRE({ chainId: 123456, providerOrSigner: config.providerOrSigner }) + }).toThrowError("Unsupported chain id") + }) + + test.each` + operatorAddress | operatorConfirmed | testMessage + ${AddressZero} | ${false} | ${"not set"} + ${account} | ${true} | ${"set"} + `( + "should return the staking provider app info if an operator is $testMessage", + async ({ operatorAddress, operatorConfirmed }) => { + const mockContractResult = { + operator: operatorAddress, + operatorConfirmed: operatorConfirmed, + operatorStartTimestamp: BigNumber.from(123), + } + mockPREContract.stakingProviderInfo.mockResolvedValue(mockContractResult) + + const result = await pre.getStakingProviderAppInfo(account) + + expect(mockPREContract.stakingProviderInfo).toHaveBeenCalledWith(account) + expect(result).toEqual({ + operator: mockContractResult.operator, + isOperatorConfirmed: mockContractResult.operatorConfirmed, + operatorStartTimestamp: + mockContractResult.operatorStartTimestamp.toString(), + isOperatorMapped: + !isAddressZero(mockContractResult.operator) && + mockContractResult.operatorConfirmed, + }) + } + ) +}) diff --git a/src/threshold-ts/applications/pre/index.ts b/src/threshold-ts/applications/pre/index.ts index 90c2a72d2..28ae2426a 100644 --- a/src/threshold-ts/applications/pre/index.ts +++ b/src/threshold-ts/applications/pre/index.ts @@ -3,7 +3,7 @@ import SimplePREApplicationABI from "./abi.json" import { AddressZero, isAddressZero, getContract } from "../../utils" import { EthereumConfig } from "../../types" -const PRE_ADDRESSESS = { +export const PRE_ADDRESSESS = { // https://etherscan.io/address/0x7E01c9c03FD3737294dbD7630a34845B0F70E5Dd 1: "0x7E01c9c03FD3737294dbD7630a34845B0F70E5Dd", // https://goerli.etherscan.io/address/0x829fdCDf6Be747FEA37518fBd83dF70EE371fCf2 From 7e17f7c0b5d57ea777f2246af7a86cb73b5d37fd Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski <rafal.czajkowski@keep.network> Date: Thu, 10 Nov 2022 15:44:42 +0100 Subject: [PATCH 6/6] Fix failing tests for mas service --- src/threshold-ts/mas/__test__/mas.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/threshold-ts/mas/__test__/mas.test.ts b/src/threshold-ts/mas/__test__/mas.test.ts index 88760890e..679cb62af 100644 --- a/src/threshold-ts/mas/__test__/mas.test.ts +++ b/src/threshold-ts/mas/__test__/mas.test.ts @@ -6,12 +6,18 @@ import { Application } from "../../applications" import { IMulticall } from "../../multicall" import { IStaking } from "../../staking" import { EthereumConfig } from "../../types" +import { IPRE, PRE } from "../../applications/pre" jest.mock("../../applications", () => ({ ...(jest.requireActual("../../applications") as {}), Application: jest.fn(), })) +jest.mock("../../applications/pre", () => ({ + ...(jest.requireActual("../../applications/pre") as {}), + PRE: jest.fn(), +})) + jest.mock("@keep-network/random-beacon/artifacts/RandomBeacon.json", () => ({ address: "0x1", abi: [], @@ -27,6 +33,8 @@ describe("Multi app staking test", () => { let multicall: IMulticall let config: EthereumConfig let mas: MultiAppStaking + let pre: IPRE + const app1 = { address: WalletRegistry.address, contract: { interface: {} }, @@ -38,9 +46,11 @@ describe("Multi app staking test", () => { beforeEach(() => { staking = {} as IStaking - multicall = { aggregate: jest.fn() } as IMulticall + multicall = { aggregate: jest.fn() } as unknown as IMulticall + pre = {} as unknown as IPRE ;(Application as unknown as jest.Mock).mockReturnValueOnce(app1) ;(Application as unknown as jest.Mock).mockReturnValueOnce(app2) + ;(PRE as unknown as jest.Mock).mockResolvedValue(pre) config = { chainId: 1, providerOrSigner: {} as providers.Provider } mas = new MultiAppStaking(staking, multicall, config) }) @@ -57,8 +67,10 @@ describe("Multi app staking test", () => { abi: WalletRegistry.abi, ...config, }) + expect(PRE).toHaveBeenCalledWith(config) expect(mas.randomBeacon).toBeDefined() expect(mas.ecdsa).toBeDefined() + expect(mas.pre).toBeDefined() }) test("should return the supported apps authroziation parameters", async () => {