From 3ec80eee129e950ece35644525104603a42fc97e Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Mon, 7 Nov 2022 15:49:10 +0100 Subject: [PATCH 01/10] Create the erc20 token wrapper Create a wrapper for erc20 token. Also create a `Tokens` class- a wrapper for all tokens related to the threshold network. --- src/threshold-ts/index.ts | 3 + .../tokens/__tests__/tokens.test.ts | 73 +++++++++ .../tokens/erc20/__tests__/erc20.test.ts | 143 ++++++++++++++++++ src/threshold-ts/tokens/erc20/index.ts | 59 ++++++++ src/threshold-ts/tokens/index.ts | 33 ++++ 5 files changed, 311 insertions(+) create mode 100644 src/threshold-ts/tokens/__tests__/tokens.test.ts create mode 100644 src/threshold-ts/tokens/erc20/__tests__/erc20.test.ts create mode 100644 src/threshold-ts/tokens/erc20/index.ts create mode 100644 src/threshold-ts/tokens/index.ts diff --git a/src/threshold-ts/index.ts b/src/threshold-ts/index.ts index 6fd493e8a..e4db426d2 100644 --- a/src/threshold-ts/index.ts +++ b/src/threshold-ts/index.ts @@ -1,6 +1,7 @@ import { MultiAppStaking } from "./mas" import { IMulticall, Multicall } from "./multicall" import { IStaking, Staking } from "./staking" +import { ITokens, Tokens } from "./tokens" import { ThresholdConfig } from "./types" import { IVendingMachines, VendingMachines } from "./vending-machine" @@ -9,6 +10,7 @@ export class Threshold { staking!: IStaking multiAppStaking!: MultiAppStaking vendingMachines!: IVendingMachines + tokens!: ITokens constructor(config: ThresholdConfig) { this._initialize(config) @@ -16,6 +18,7 @@ export class Threshold { private _initialize = (config: ThresholdConfig) => { this.multicall = new Multicall(config.ethereum) + this.tokens = new Tokens(config.ethereum) this.vendingMachines = new VendingMachines(config.ethereum) this.staking = new Staking( config.ethereum, diff --git a/src/threshold-ts/tokens/__tests__/tokens.test.ts b/src/threshold-ts/tokens/__tests__/tokens.test.ts new file mode 100644 index 000000000..e587a78ce --- /dev/null +++ b/src/threshold-ts/tokens/__tests__/tokens.test.ts @@ -0,0 +1,73 @@ +import T from "@threshold-network/solidity-contracts/artifacts/T.json" +import NuCypherToken from "@threshold-network/solidity-contracts/artifacts/NuCypherToken.json" +import KeepToken from "@keep-network/keep-core/artifacts/KeepToken.json" +import { ethers } from "ethers" +import { ITokens, Tokens } from ".." +import { ERC20TokenWithApproveAndCall } from "../erc20" +import { EthereumConfig } from "../../types" +import { getContractAddressFromTruffleArtifact } from "../../utils" + +jest.mock("../erc20", () => ({ + ...(jest.requireActual("../erc20") as {}), + ERC20TokenWithApproveAndCall: jest.fn(), +})) + +jest.mock("../../utils", () => ({ + ...(jest.requireActual("../../utils") as {}), + getContractAddressFromTruffleArtifact: jest.fn(), +})) + +jest.mock("@threshold-network/solidity-contracts/artifacts/T.json", () => ({ + address: "0x6A55B762689Ba514569E565E439699aBC731f156", + abi: [], +})) + +jest.mock( + "@threshold-network/solidity-contracts/artifacts/NuCypherToken.json", + () => ({ + address: "0xd696d5a9b083959587F30e487038529a876b08C2", + abi: [], + }) +) + +jest.mock("@keep-network/keep-core/artifacts/KeepToken.json", () => ({ + address: "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + abi: [], +})) + +describe("ERC20 token test", () => { + let tokens: ITokens + let config: EthereumConfig + const account = "0xaC1933A3Ee78A26E16030801273fBa250631eD5f" + const keepTokenAddress = (KeepToken as unknown as { address: string }).address + + beforeEach(() => { + config = { + providerOrSigner: {} as ethers.providers.Provider, + chainId: 1, + account, + } + ;(getContractAddressFromTruffleArtifact as jest.Mock).mockReturnValue( + keepTokenAddress + ) + tokens = new Tokens(config) + }) + + test("should create the Tokens wrapper correctly", () => { + expect(ERC20TokenWithApproveAndCall).toHaveBeenNthCalledWith(1, config, { + address: T.address, + abi: T.abi, + }) + expect(ERC20TokenWithApproveAndCall).toHaveBeenNthCalledWith(2, config, { + address: NuCypherToken.address, + abi: NuCypherToken.abi, + }) + expect(ERC20TokenWithApproveAndCall).toHaveBeenNthCalledWith(3, config, { + address: keepTokenAddress, + abi: KeepToken.abi, + }) + expect(tokens.t).toBeInstanceOf(ERC20TokenWithApproveAndCall) + expect(tokens.keep).toBeInstanceOf(ERC20TokenWithApproveAndCall) + expect(tokens.nu).toBeInstanceOf(ERC20TokenWithApproveAndCall) + }) +}) diff --git a/src/threshold-ts/tokens/erc20/__tests__/erc20.test.ts b/src/threshold-ts/tokens/erc20/__tests__/erc20.test.ts new file mode 100644 index 000000000..8e2a0827c --- /dev/null +++ b/src/threshold-ts/tokens/erc20/__tests__/erc20.test.ts @@ -0,0 +1,143 @@ +import { BigNumber, ContractTransaction, ethers } from "ethers" +import { + BaseERC20Token, + ERC20TokenWithApproveAndCall, + IERC20, + IERC20WithApproveAndCall, +} from ".." +import { EthereumConfig } from "../../../types" +import { getContract } from "../../../utils" + +jest.mock("../../../utils", () => ({ + ...(jest.requireActual("../../../utils") as {}), + getContract: jest.fn(), +})) + +describe("ERC20 token test", () => { + let config: EthereumConfig + let artifact: { abi: any; address: string } + + const account = "0xaC1933A3Ee78A26E16030801273fBa250631eD5f" + const spender = "0x6c7960687253e43e98A0d3d602dD5085d2443e75" + const amount = BigNumber.from("100000000") + + beforeEach(() => { + artifact = { + abi: [], + address: "0xC49C8567DE3Cd9aA28c36b88dFb2A0EfF3BE41cE", + } + + config = { + providerOrSigner: {} as ethers.providers.Provider, + chainId: 1, + account, + } + }) + + describe("Base ERC20 token test", () => { + let erc20: IERC20 + let mockErc20TokenContract: { + balanceOf: jest.MockedFn + allowance: jest.MockedFn + totalSupply: jest.MockedFn + } + + beforeEach(() => { + mockErc20TokenContract = { + balanceOf: jest.fn(), + allowance: jest.fn(), + totalSupply: jest.fn(), + } + ;(getContract as jest.Mock).mockImplementation( + () => mockErc20TokenContract + ) + + erc20 = new BaseERC20Token(config, artifact) + }) + + test("should create the base erc20 token instance", () => { + expect(getContract).toHaveBeenCalledWith( + artifact.address, + artifact.abi, + config.providerOrSigner, + config.account + ) + expect(erc20.contract).toEqual(mockErc20TokenContract) + }) + + test("should return balance of a given address", async () => { + mockErc20TokenContract.balanceOf.mockResolvedValue(amount) + + const result = await erc20.balanceOf(account) + + expect(mockErc20TokenContract.balanceOf).toHaveBeenCalledWith(account) + expect(result).toEqual(amount) + }) + test("should return allowed amount of tokens that spender will be allowed to spend on behalf of owner", async () => { + mockErc20TokenContract.allowance.mockResolvedValue(amount) + + const result = await erc20.allowance(account, spender) + + expect(mockErc20TokenContract.allowance).toHaveBeenCalledWith( + account, + spender + ) + expect(result).toEqual(amount) + }) + + test("should return the total supply of token", async () => { + mockErc20TokenContract.totalSupply.mockResolvedValue(amount) + + const result = await erc20.totalSupply() + + expect(mockErc20TokenContract.totalSupply).toHaveBeenCalled() + expect(result).toEqual(amount) + }) + }) + + describe("ERC20 with approve and call pattern test", () => { + let erc20withApproveAndCall: IERC20WithApproveAndCall + let mockErc20TokenContract: { + balanceOf: jest.MockedFn + allowance: jest.MockedFn + totalSupply: jest.MockedFn + approveAndCall: jest.MockedFn + } + + beforeEach(() => { + mockErc20TokenContract = { + balanceOf: jest.fn(), + allowance: jest.fn(), + totalSupply: jest.fn(), + approveAndCall: jest.fn(), + } + ;(getContract as jest.Mock).mockImplementation( + () => mockErc20TokenContract + ) + + erc20withApproveAndCall = new ERC20TokenWithApproveAndCall( + config, + artifact + ) + }) + + test("should call the approve and call correctly", async () => { + const extraData = "0x123456789" + const mockTx = {} as ContractTransaction + mockErc20TokenContract.approveAndCall.mockResolvedValue(mockTx) + + const result = await erc20withApproveAndCall.approveAndCall( + spender, + amount.toString(), + extraData + ) + + expect(mockErc20TokenContract.approveAndCall).toHaveBeenCalledWith( + spender, + amount.toString(), + extraData + ) + expect(result).toEqual(mockTx) + }) + }) +}) diff --git a/src/threshold-ts/tokens/erc20/index.ts b/src/threshold-ts/tokens/erc20/index.ts new file mode 100644 index 000000000..d8ae6c85d --- /dev/null +++ b/src/threshold-ts/tokens/erc20/index.ts @@ -0,0 +1,59 @@ +import { BigNumber, Contract, ContractTransaction } from "ethers" +import { EthereumConfig } from "../../types" +import { getContract } from "../../utils" + +export interface IERC20 { + contract: Contract + balanceOf: (account: string) => Promise + allowance: (owner: string, spender: string) => Promise + totalSupply: () => Promise +} + +export interface IERC20WithApproveAndCall extends IERC20 { + approveAndCall: ( + spender: string, + amount: string, + extraData: string + ) => Promise +} + +export class BaseERC20Token implements IERC20 { + protected _contract: Contract + + constructor(config: EthereumConfig, artifact: { abi: any; address: string }) { + this._contract = getContract( + artifact.address, + artifact.abi, + config.providerOrSigner, + config.account + ) + } + balanceOf = (account: string): Promise => { + return this._contract.balanceOf(account) + } + + allowance = (owner: string, spender: string): Promise => { + return this._contract.allowance(owner, spender) + } + + totalSupply = (): Promise => { + return this._contract.totalSupply() + } + + get contract() { + return this._contract + } +} + +export class ERC20TokenWithApproveAndCall + extends BaseERC20Token + implements IERC20WithApproveAndCall +{ + approveAndCall = async ( + spender: string, + amount: string, + extraData: string + ): Promise => { + return await this._contract.approveAndCall(spender, amount, extraData) + } +} diff --git a/src/threshold-ts/tokens/index.ts b/src/threshold-ts/tokens/index.ts new file mode 100644 index 000000000..05417dff3 --- /dev/null +++ b/src/threshold-ts/tokens/index.ts @@ -0,0 +1,33 @@ +import T from "@threshold-network/solidity-contracts/artifacts/T.json" +import NuCypherToken from "@threshold-network/solidity-contracts/artifacts/NuCypherToken.json" +import KeepToken from "@keep-network/keep-core/artifacts/KeepToken.json" +import { IERC20WithApproveAndCall, ERC20TokenWithApproveAndCall } from "./erc20" +import { EthereumConfig } from "../types" +import { getContractAddressFromTruffleArtifact } from "../utils" + +export interface ITokens { + t: IERC20WithApproveAndCall + keep: IERC20WithApproveAndCall + nu: IERC20WithApproveAndCall +} + +export class Tokens implements ITokens { + public readonly t: IERC20WithApproveAndCall + public readonly nu: IERC20WithApproveAndCall + public readonly keep: IERC20WithApproveAndCall + + constructor(config: EthereumConfig) { + this.t = new ERC20TokenWithApproveAndCall(config, { + address: T.address, + abi: T.abi, + }) + this.nu = new ERC20TokenWithApproveAndCall(config, { + address: NuCypherToken.address, + abi: NuCypherToken.abi, + }) + this.keep = new ERC20TokenWithApproveAndCall(config, { + address: getContractAddressFromTruffleArtifact(KeepToken), + abi: KeepToken.abi, + }) + } +} From 18f715c69ce239d68a3132c8ac1e1631d3df916e Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 8 Nov 2022 13:24:27 +0100 Subject: [PATCH 02/10] Clean up tokens redux state The main change are: - remove the unnecessary `conversionRate` from a token state, only keep and nu has conversion rate to T but the store for tokens must be generic so it's unnecessary to define `conversionRate` for each token. We have nice solution for storing the vending machine ratio in local storage. - clean up the token context- fetching the token balances using the threshold-ts lib using listener middleware. It's a one-shot listener and effect callback will be run on `walletConnected` action. We will probably get rid of the `TokenContext` in a follow-up work. --- src/contexts/TokenContext.tsx | 62 +----------------- src/hooks/useTokenState.ts | 26 +++----- src/pages/Overview/Network/WalletBalances.tsx | 9 ++- src/store/index.ts | 3 +- src/store/tokens/effects.ts | 63 +++++++++++++++++++ src/store/tokens/tokenSlice.ts | 37 +++++------ src/types/token.ts | 20 +----- 7 files changed, 96 insertions(+), 124 deletions(-) create mode 100644 src/store/tokens/effects.ts diff --git a/src/contexts/TokenContext.tsx b/src/contexts/TokenContext.tsx index 68467c305..855f34eef 100644 --- a/src/contexts/TokenContext.tsx +++ b/src/contexts/TokenContext.tsx @@ -1,19 +1,15 @@ import React, { createContext } from "react" import { Contract } from "@ethersproject/contracts" -import { AddressZero } from "@ethersproject/constants" import { useWeb3React } from "@web3-react/core" import { useKeep } from "../web3/hooks/useKeep" import { useNu } from "../web3/hooks/useNu" import { useT } from "../web3/hooks/useT" import { useTokenState } from "../hooks/useTokenState" -import { useTokensBalanceCall } from "../hooks/useTokensBalanceCall" import { Token } from "../enums" import { TokenState } from "../types" import { useTBTCTokenContract } from "../web3/hooks" -import { useVendingMachineRatio } from "../web3/hooks/useVendingMachineRatio" import { useFetchOwnerStakes } from "../hooks/useFetchOwnerStakes" import { useTBTCv2TokenContract } from "../web3/hooks/useTBTCv2TokenContract" -import { featureFlags } from "../constants" interface TokenContextState extends TokenState { contract: Contract | null @@ -37,15 +33,10 @@ export const TokenContextProvider: React.FC = ({ children }) => { const t = useT() const tbtc = useTBTCTokenContract() const tbtcv2 = useTBTCv2TokenContract() - const nuConversion = useVendingMachineRatio(Token.Nu) - const keepConversion = useVendingMachineRatio(Token.Keep) - const { active, chainId, account } = useWeb3React() + const { account } = useWeb3React() const fetchOwnerStakes = useFetchOwnerStakes() const { - fetchTokenPriceUSD, - setTokenBalance, - setTokenConversionRate, keep: keepData, nu: nuData, t: tData, @@ -57,57 +48,6 @@ export const TokenContextProvider: React.FC = ({ children }) => { if (!!tbtcv2) tokenContracts.push(tbtcv2.contract) - const fetchBalances = useTokensBalanceCall( - tokenContracts, - active ? account! : AddressZero - ) - - // - // SET T CONVERSION RATE FOR KEEP, NU - // - React.useEffect(() => { - setTokenConversionRate(Token.Nu, nuConversion) - setTokenConversionRate(Token.Keep, keepConversion) - }, [nuConversion, keepConversion]) - - // - // SET USD PRICE - // - React.useEffect(() => { - for (const token in Token) { - if (token) { - // @ts-ignore - fetchTokenPriceUSD(Token[token]) - } - } - }, []) - - // - // FETCH BALANCES ON WALLET LOAD OR NETWORK SWITCH - // - React.useEffect(() => { - if (active) { - fetchBalances().then( - ([keepBalance, nuBalance, tBalance, tbtcv2Balance]) => { - setTokenBalance(Token.Keep, keepBalance.toString()) - setTokenBalance(Token.Nu, nuBalance.toString()) - setTokenBalance(Token.T, tBalance.toString()) - if (featureFlags.TBTC_V2) { - setTokenBalance(Token.TBTCV2, tbtcv2Balance.toString()) - } - } - ) - } else { - // set all token balances to 0 if the user disconnects the wallet - for (const token in Token) { - if (token) { - // @ts-ignore - setTokenBalance(Token[token], 0) - } - } - } - }, [active, chainId, account]) - // fetch user stakes when they connect their wallet React.useEffect(() => { fetchOwnerStakes(account!) diff --git a/src/hooks/useTokenState.ts b/src/hooks/useTokenState.ts index 7e1629d78..02e643628 100644 --- a/src/hooks/useTokenState.ts +++ b/src/hooks/useTokenState.ts @@ -1,33 +1,26 @@ -import { useSelector, useDispatch } from "react-redux" import { setTokenBalance as setTokenBalanceAction, setTokenLoading as setTokenLoadingAction, fetchTokenPriceUSD as fetchTokenPriceAction, - setTokenConversionRate as setTokenConversionRateAction, } from "../store/tokens" -import { RootState } from "../store" +import { useAppDispatch, useAppSelector } from "./store" import { Token } from "../enums" import { UseTokenState } from "../types/token" export const useTokenState: UseTokenState = () => { - const keep = useSelector((state: RootState) => state.token[Token.Keep]) - const nu = useSelector((state: RootState) => state.token[Token.Nu]) - const t = useSelector((state: RootState) => state.token[Token.T]) - const tbtc = useSelector((state: RootState) => state.token[Token.TBTC]) - const tbtcv2 = useSelector((state: RootState) => state.token[Token.TBTCV2]) + const keep = useAppSelector((state) => state.token[Token.Keep]) + const nu = useAppSelector((state) => state.token[Token.Nu]) + const t = useAppSelector((state) => state.token[Token.T]) + const tbtc = useAppSelector((state) => state.token[Token.TBTC]) + const tbtcv2 = useAppSelector((state) => state.token[Token.TBTCV2]) - const dispatch = useDispatch() - - const setTokenConversionRate = ( - token: Token, - conversionRate: number | string - ) => dispatch(setTokenConversionRateAction({ token, conversionRate })) + const dispatch = useAppDispatch() const setTokenBalance = (token: Token, balance: number | string) => dispatch(setTokenBalanceAction({ token, balance })) - const setTokenLoading = (token: Token, loading: boolean) => - dispatch(setTokenLoadingAction({ token, loading })) + const setTokenLoading = (token: Token) => + dispatch(setTokenLoadingAction({ token })) const fetchTokenPriceUSD = (token: Token) => dispatch(fetchTokenPriceAction({ token })) @@ -41,6 +34,5 @@ export const useTokenState: UseTokenState = () => { fetchTokenPriceUSD, setTokenBalance, setTokenLoading, - setTokenConversionRate, } } diff --git a/src/pages/Overview/Network/WalletBalances.tsx b/src/pages/Overview/Network/WalletBalances.tsx index 0b72fdd4f..0a8a6abd1 100644 --- a/src/pages/Overview/Network/WalletBalances.tsx +++ b/src/pages/Overview/Network/WalletBalances.tsx @@ -19,6 +19,7 @@ import InfoBox from "../../../components/InfoBox" import Link from "../../../components/Link" import useUpgradeHref from "../../../hooks/useUpgradeHref" import ButtonLink from "../../../components/ButtonLink" +import { useTExchangeRate } from "../../../hooks/useTExchangeRate" const BalanceStat: FC<{ balance: string | number @@ -39,7 +40,7 @@ const BalanceStat: FC<{ /> {conversionRate && ( - 1 {text} = {conversionRate} T + 1 {text} = {formatTokenAmount(conversionRate, "0.0000")} T )} @@ -86,6 +87,8 @@ const WalletBalances: FC = () => { const { amount: keepToT } = useTConvertedAmount(Token.Keep, keep.balance) const { amount: nuToT } = useTConvertedAmount(Token.Nu, nu.balance) + const { amount: keepToTConversionRate } = useTExchangeRate(Token.Keep) + const { amount: nuToTConversionRate } = useTExchangeRate(Token.Nu) const conversionToTAmount = useMemo(() => { return BigNumber.from(keepToT).add(nuToT).toString() @@ -105,14 +108,14 @@ const WalletBalances: FC = () => { )} { registerStakingListeners() registerStakingAppsListeners() registerAccountListeners() + registerTokensListeners() state = { eth: { ...state.eth }, token: { diff --git a/src/store/tokens/effects.ts b/src/store/tokens/effects.ts new file mode 100644 index 000000000..3aaf75b2d --- /dev/null +++ b/src/store/tokens/effects.ts @@ -0,0 +1,63 @@ +import { BigNumber } from "ethers" +import { Token } from "../../enums" +import { isAddress } from "../../web3/utils" +import { walletConnected } from "../account" +import { AppListenerEffectAPI } from "../listener" +import { + setTokenBalance, + setTokenLoading, + fetchTokenPriceUSD, +} from "./tokenSlice" + +export const fetchTokenBalances = async ( + actionCreator: ReturnType, + listenerApi: AppListenerEffectAPI +) => { + const address = actionCreator.payload + if (!isAddress(address)) return + + const { keep, nu, t } = listenerApi.extra.threshold.tokens + + const tokens = [ + { token: keep, name: Token.Keep }, + { token: nu, name: Token.Nu }, + { token: t, name: Token.T }, + ] + listenerApi.unsubscribe() + try { + tokens.forEach((_) => { + listenerApi.dispatch( + setTokenLoading({ + token: _.name, + }) + ) + }) + + const balances: BigNumber[] = + await listenerApi.extra.threshold.multicall.aggregate( + tokens + .map((_) => _.token) + .map((_) => ({ + interface: _.contract.interface, + address: _.contract.address, + method: "balanceOf", + args: [address], + })) + ) + + tokens.forEach((_, index) => { + listenerApi.dispatch( + setTokenBalance({ token: _.name, balance: balances[index].toString() }) + ) + }) + + tokens + .map((_) => _.name) + .forEach((tokenName) => + listenerApi.dispatch(fetchTokenPriceUSD({ token: tokenName })) + ) + } catch (error) { + console.error("Could not fetch token balances", error) + listenerApi.subscribe() + } +} diff --git a/src/store/tokens/tokenSlice.ts b/src/store/tokens/tokenSlice.ts index daf7ebabe..5c8124ebb 100644 --- a/src/store/tokens/tokenSlice.ts +++ b/src/store/tokens/tokenSlice.ts @@ -1,19 +1,17 @@ -import numeral from "numeral" -import axios from "axios" -import { FixedNumber } from "@ethersproject/bignumber" -import { formatUnits } from "@ethersproject/units" import { PayloadAction } from "@reduxjs/toolkit/dist/createAction" import { createAsyncThunk, createSlice } from "@reduxjs/toolkit" import { CoingeckoID, Token } from "../../enums/token" import { TokenState, SetTokenBalanceActionPayload, - SetTokenConversionRateActionPayload, SetTokenLoadingActionPayload, } from "../../types/token" import { exchangeAPI } from "../../utils/exchangeAPI" import Icon from "../../enums/icon" import getUsdBalance from "../../utils/getUsdBalance" +import { startAppListening } from "../listener" +import { walletConnected } from "../account" +import { fetchTokenBalances } from "./effects" export const fetchTokenPriceUSD = createAsyncThunk( "tokens/fetchTokenPriceUSD", @@ -30,7 +28,6 @@ export const tokenSlice = createSlice({ [Token.Keep]: { loading: false, balance: 0, - conversionRate: 4.87, text: Token.Keep, icon: Icon.KeepCircleBrand, usdConversion: 0, @@ -39,7 +36,6 @@ export const tokenSlice = createSlice({ [Token.Nu]: { loading: false, balance: 0, - conversionRate: 2.66, text: Token.Nu, icon: Icon.NuCircleBrand, usdConversion: 0, @@ -48,7 +44,6 @@ export const tokenSlice = createSlice({ [Token.T]: { loading: false, balance: 0, - conversionRate: 1, text: Token.T, icon: Icon.TCircleBrand, usdConversion: 0, @@ -72,31 +67,20 @@ export const tokenSlice = createSlice({ state, action: PayloadAction ) => { - state[action.payload.token].loading = action.payload.loading + state[action.payload.token].loading = true }, setTokenBalance: ( state, action: PayloadAction ) => { const { token, balance } = action.payload + state[token].loading = false state[token].balance = balance state[token].usdBalance = getUsdBalance( state[token].balance, state[token].usdConversion ) }, - setTokenConversionRate: ( - state, - action: PayloadAction - ) => { - const { token, conversionRate } = action.payload - - const formattedConversionRate = numeral( - +conversionRate / 10 ** 15 - ).format("0.0000") - - state[token].conversionRate = formattedConversionRate - }, }, extraReducers: (builder) => { builder.addCase(fetchTokenPriceUSD.fulfilled, (state, action) => { @@ -111,5 +95,12 @@ export const tokenSlice = createSlice({ }, }) -export const { setTokenBalance, setTokenLoading, setTokenConversionRate } = - tokenSlice.actions +export const { setTokenBalance, setTokenLoading } = tokenSlice.actions + +export const registerTokensListeners = () => { + startAppListening({ + actionCreator: walletConnected, + effect: fetchTokenBalances, + }) +} +registerTokensListeners() diff --git a/src/types/token.ts b/src/types/token.ts index 3dde6a7a1..97cf518cc 100644 --- a/src/types/token.ts +++ b/src/types/token.ts @@ -5,7 +5,6 @@ import Icon from "../enums/icon" export interface TokenState { loading: boolean - conversionRate: number | string text: string icon: Icon balance: number | string @@ -19,14 +18,8 @@ export interface SetTokenBalanceActionPayload { balance: number | string } -export interface SetTokenConversionRateActionPayload { - token: Token - conversionRate: string | number -} - export interface SetTokenLoadingActionPayload { token: Token - loading: boolean } export interface SetTokenBalance { @@ -37,14 +30,7 @@ export interface SetTokenLoading { payload: SetTokenLoadingActionPayload } -export interface SetTokenConversionRate { - payload: SetTokenConversionRateActionPayload -} - -export type TokenActionTypes = - | SetTokenBalance - | SetTokenLoading - | SetTokenConversionRate +export type TokenActionTypes = SetTokenBalance | SetTokenLoading export interface UseTokenState { (): { @@ -57,10 +43,6 @@ export interface UseTokenState { token: Token, balance: number | string ) => TokenActionTypes - setTokenConversionRate: ( - token: Token, - conversionRate: number | string - ) => TokenActionTypes setTokenLoading: (token: Token, loading: boolean) => TokenActionTypes fetchTokenPriceUSD: (token: Token) => void } From 3b1b8a4eb94219873e9d6f0c829b684b8e63d0ee Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 8 Nov 2022 15:00:59 +0100 Subject: [PATCH 03/10] Refactor token hooks Use the ERC20 wrapper from the threshold ts lib. --- src/contexts/TokenContext.tsx | 4 -- src/hooks/useTokenState.ts | 27 ++++++-- src/hooks/useTransaction.ts | 10 +-- src/store/tokens/effects.ts | 16 ++++- src/store/tokens/tokenSlice.ts | 11 ++- src/threshold-ts/tokens/erc20/index.ts | 5 ++ src/threshold-ts/tokens/index.ts | 14 ++++ src/types/token.ts | 12 +++- src/web3/hooks/useERC20.ts | 85 ++++++------------------ src/web3/hooks/useKeep.ts | 35 ++-------- src/web3/hooks/useNu.ts | 35 ++-------- src/web3/hooks/useT.ts | 36 ++-------- src/web3/hooks/useTBTCTokenContract.ts | 7 +- src/web3/hooks/useTBTCv2TokenContract.ts | 31 ++------- 14 files changed, 124 insertions(+), 204 deletions(-) diff --git a/src/contexts/TokenContext.tsx b/src/contexts/TokenContext.tsx index 855f34eef..3556d5143 100644 --- a/src/contexts/TokenContext.tsx +++ b/src/contexts/TokenContext.tsx @@ -44,10 +44,6 @@ export const TokenContextProvider: React.FC = ({ children }) => { tbtcv2: tbtcv2Data, } = useTokenState() - const tokenContracts = [keep.contract!, nu.contract!, t.contract!] - - if (!!tbtcv2) tokenContracts.push(tbtcv2.contract) - // fetch user stakes when they connect their wallet React.useEffect(() => { fetchOwnerStakes(account!) diff --git a/src/hooks/useTokenState.ts b/src/hooks/useTokenState.ts index 02e643628..585371808 100644 --- a/src/hooks/useTokenState.ts +++ b/src/hooks/useTokenState.ts @@ -2,10 +2,12 @@ import { setTokenBalance as setTokenBalanceAction, setTokenLoading as setTokenLoadingAction, fetchTokenPriceUSD as fetchTokenPriceAction, + setTokenBalanceError as setTokenBalanceErrorAction, } from "../store/tokens" import { useAppDispatch, useAppSelector } from "./store" import { Token } from "../enums" import { UseTokenState } from "../types/token" +import { useCallback } from "react" export const useTokenState: UseTokenState = () => { const keep = useAppSelector((state) => state.token[Token.Keep]) @@ -16,14 +18,26 @@ export const useTokenState: UseTokenState = () => { const dispatch = useAppDispatch() - const setTokenBalance = (token: Token, balance: number | string) => - dispatch(setTokenBalanceAction({ token, balance })) + const setTokenBalance = useCallback( + (token: Token, balance: number | string) => + dispatch(setTokenBalanceAction({ token, balance })), + [dispatch] + ) - const setTokenLoading = (token: Token) => - dispatch(setTokenLoadingAction({ token })) + const setTokenLoading = useCallback( + (token: Token) => dispatch(setTokenLoadingAction({ token })), + [dispatch] + ) - const fetchTokenPriceUSD = (token: Token) => - dispatch(fetchTokenPriceAction({ token })) + const fetchTokenPriceUSD = useCallback( + (token: Token) => dispatch(fetchTokenPriceAction({ token })), + [dispatch] + ) + + const setTokenBalanceError = useCallback( + (token: Token) => dispatch(setTokenBalanceErrorAction({ token })), + [dispatch] + ) return { keep, @@ -34,5 +48,6 @@ export const useTokenState: UseTokenState = () => { fetchTokenPriceUSD, setTokenBalance, setTokenLoading, + setTokenBalanceError, } } diff --git a/src/hooks/useTransaction.ts b/src/hooks/useTransaction.ts index c7a275d32..d23c85ec8 100644 --- a/src/hooks/useTransaction.ts +++ b/src/hooks/useTransaction.ts @@ -3,6 +3,7 @@ import { setTransactionStatus as setTransactionStatusAction } from "../store/tra import { RootState } from "../store" import { UseTransaction } from "../types/transaction" import { TransactionStatus, TransactionType } from "../enums/transactionType" +import { useCallback } from "react" export const useTransaction: UseTransaction = () => { const keepApproval = useSelector( @@ -20,10 +21,11 @@ export const useTransaction: UseTransaction = () => { const dispatch = useDispatch() - const setTransactionStatus = ( - type: TransactionType, - status: TransactionStatus - ) => dispatch(setTransactionStatusAction({ type, status })) + const setTransactionStatus = useCallback( + (type: TransactionType, status: TransactionStatus) => + dispatch(setTransactionStatusAction({ type, status })), + [dispatch] + ) return { setTransactionStatus, diff --git a/src/store/tokens/effects.ts b/src/store/tokens/effects.ts index 3aaf75b2d..5b4fb2fd6 100644 --- a/src/store/tokens/effects.ts +++ b/src/store/tokens/effects.ts @@ -1,4 +1,5 @@ import { BigNumber } from "ethers" +import { featureFlags } from "../../constants" import { Token } from "../../enums" import { isAddress } from "../../web3/utils" import { walletConnected } from "../account" @@ -7,6 +8,7 @@ import { setTokenBalance, setTokenLoading, fetchTokenPriceUSD, + setTokenBalanceError, } from "./tokenSlice" export const fetchTokenBalances = async ( @@ -16,13 +18,20 @@ export const fetchTokenBalances = async ( const address = actionCreator.payload if (!isAddress(address)) return - const { keep, nu, t } = listenerApi.extra.threshold.tokens + const { keep, nu, t, tbtc, tbtcv1 } = listenerApi.extra.threshold.tokens const tokens = [ { token: keep, name: Token.Keep }, { token: nu, name: Token.Nu }, { token: t, name: Token.T }, + { token: tbtcv1, name: Token.TBTC }, + { token: tbtc, name: Token.TBTCV2 }, ] + + if (featureFlags.TBTC_V2) { + tokens.push({ token: tbtc, name: Token.TBTCV2 }) + } + listenerApi.unsubscribe() try { tokens.forEach((_) => { @@ -58,6 +67,11 @@ export const fetchTokenBalances = async ( ) } catch (error) { console.error("Could not fetch token balances", error) + tokens + .map((_) => _.name) + .forEach((tokenName) => + listenerApi.dispatch(setTokenBalanceError({ token: tokenName })) + ) listenerApi.subscribe() } } diff --git a/src/store/tokens/tokenSlice.ts b/src/store/tokens/tokenSlice.ts index 5c8124ebb..b84c38a66 100644 --- a/src/store/tokens/tokenSlice.ts +++ b/src/store/tokens/tokenSlice.ts @@ -5,6 +5,7 @@ import { TokenState, SetTokenBalanceActionPayload, SetTokenLoadingActionPayload, + SetTokenBalanceErrorActionPayload, } from "../../types/token" import { exchangeAPI } from "../../utils/exchangeAPI" import Icon from "../../enums/icon" @@ -81,6 +82,13 @@ export const tokenSlice = createSlice({ state[token].usdConversion ) }, + setTokenBalanceError: ( + state, + action: PayloadAction + ) => { + const { token } = action.payload + state[token].loading = false + }, }, extraReducers: (builder) => { builder.addCase(fetchTokenPriceUSD.fulfilled, (state, action) => { @@ -95,7 +103,8 @@ export const tokenSlice = createSlice({ }, }) -export const { setTokenBalance, setTokenLoading } = tokenSlice.actions +export const { setTokenBalance, setTokenLoading, setTokenBalanceError } = + tokenSlice.actions export const registerTokensListeners = () => { startAppListening({ diff --git a/src/threshold-ts/tokens/erc20/index.ts b/src/threshold-ts/tokens/erc20/index.ts index d8ae6c85d..48c03d2d5 100644 --- a/src/threshold-ts/tokens/erc20/index.ts +++ b/src/threshold-ts/tokens/erc20/index.ts @@ -6,6 +6,7 @@ export interface IERC20 { contract: Contract balanceOf: (account: string) => Promise allowance: (owner: string, spender: string) => Promise + approve: (spender: string, amount: string) => Promise totalSupply: () => Promise } @@ -40,6 +41,10 @@ export class BaseERC20Token implements IERC20 { return this._contract.totalSupply() } + approve = (spender: string, amount: string): Promise => { + return this._contract.approve(spender, amount) + } + get contract() { return this._contract } diff --git a/src/threshold-ts/tokens/index.ts b/src/threshold-ts/tokens/index.ts index 05417dff3..b05b454d3 100644 --- a/src/threshold-ts/tokens/index.ts +++ b/src/threshold-ts/tokens/index.ts @@ -1,6 +1,8 @@ import T from "@threshold-network/solidity-contracts/artifacts/T.json" import NuCypherToken from "@threshold-network/solidity-contracts/artifacts/NuCypherToken.json" import KeepToken from "@keep-network/keep-core/artifacts/KeepToken.json" +import TBTCV1Token from "@keep-network/tbtc/artifacts/TBTCToken.json" +import TBTC from "@keep-network/tbtc-v2/artifacts/TBTC.json" import { IERC20WithApproveAndCall, ERC20TokenWithApproveAndCall } from "./erc20" import { EthereumConfig } from "../types" import { getContractAddressFromTruffleArtifact } from "../utils" @@ -9,12 +11,16 @@ export interface ITokens { t: IERC20WithApproveAndCall keep: IERC20WithApproveAndCall nu: IERC20WithApproveAndCall + tbtcv1: IERC20WithApproveAndCall + tbtc: IERC20WithApproveAndCall } export class Tokens implements ITokens { public readonly t: IERC20WithApproveAndCall public readonly nu: IERC20WithApproveAndCall public readonly keep: IERC20WithApproveAndCall + public readonly tbtcv1: IERC20WithApproveAndCall + public readonly tbtc: IERC20WithApproveAndCall constructor(config: EthereumConfig) { this.t = new ERC20TokenWithApproveAndCall(config, { @@ -29,5 +35,13 @@ export class Tokens implements ITokens { address: getContractAddressFromTruffleArtifact(KeepToken), abi: KeepToken.abi, }) + this.tbtcv1 = new ERC20TokenWithApproveAndCall(config, { + address: getContractAddressFromTruffleArtifact(TBTCV1Token), + abi: TBTCV1Token.abi, + }) + this.tbtc = new ERC20TokenWithApproveAndCall(config, { + address: TBTC.address, + abi: TBTC.abi, + }) } } diff --git a/src/types/token.ts b/src/types/token.ts index 97cf518cc..d02ef38c8 100644 --- a/src/types/token.ts +++ b/src/types/token.ts @@ -2,6 +2,7 @@ import { Contract } from "@ethersproject/contracts" import { Token } from "../enums" import { TransactionType } from "../enums/transactionType" import Icon from "../enums/icon" +import { IERC20, IERC20WithApproveAndCall } from "../threshold-ts/tokens/erc20" export interface TokenState { loading: boolean @@ -18,6 +19,10 @@ export interface SetTokenBalanceActionPayload { balance: number | string } +export interface SetTokenBalanceErrorActionPayload { + token: Token +} + export interface SetTokenLoadingActionPayload { token: Token } @@ -45,6 +50,7 @@ export interface UseTokenState { ) => TokenActionTypes setTokenLoading: (token: Token, loading: boolean) => TokenActionTypes fetchTokenPriceUSD: (token: Token) => void + setTokenBalanceError: (token: Token) => TokenActionTypes } } @@ -53,13 +59,13 @@ export interface BalanceOf { } export interface Approve { - (transactionType: TransactionType): any + (spender: string, amount: string): any } export interface UseErc20Interface { - (tokenAddress: string, withSignerIfPossible?: boolean, abi?: any): { - approve: Approve + (token: IERC20WithApproveAndCall | IERC20, tokenName: Token): { balanceOf: BalanceOf + wrapper: IERC20 | IERC20WithApproveAndCall contract: Contract | null } } diff --git a/src/web3/hooks/useERC20.ts b/src/web3/hooks/useERC20.ts index 51febb27c..4d689a22b 100644 --- a/src/web3/hooks/useERC20.ts +++ b/src/web3/hooks/useERC20.ts @@ -1,76 +1,35 @@ import { useCallback } from "react" -import { MaxUint256 } from "@ethersproject/constants" -import { useWeb3React } from "@web3-react/core" -import { useContract } from "./useContract" -import ERC20_ABI from "../abi/ERC20.json" import { Token } from "../../enums" import { useTokenState } from "../../hooks/useTokenState" -import { Approve, UseErc20Interface } from "../../types/token" -import { useTransaction } from "../../hooks/useTransaction" -import { TransactionStatus } from "../../enums/transactionType" -import { isWalletRejectionError } from "../../utils/isWalletRejectionError" -import { once } from "@storybook/node-logger" +import { UseErc20Interface } from "../../types/token" +import { + IERC20, + IERC20WithApproveAndCall, +} from "../../threshold-ts/tokens/erc20" export const useErc20TokenContract: UseErc20Interface = ( - tokenAddress, - withSignerIfPossible, - abi = ERC20_ABI + token: IERC20WithApproveAndCall | IERC20, + tokenName: Token ) => { - const { account } = useWeb3React() - const { setTokenLoading, setTokenBalance } = useTokenState() - const { setTransactionStatus } = useTransaction() - - // TODO: Figure out how to type the ERC20 contract - // return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible) - const contract = useContract(tokenAddress, abi, withSignerIfPossible) - - const approve: Approve = useCallback( - async (transactionType) => { - if (account) { - try { - setTransactionStatus(transactionType, TransactionStatus.PendingWallet) - const tx = await contract?.approve( - tokenAddress, - MaxUint256.toString() - ) - setTransactionStatus( - transactionType, - TransactionStatus.PendingOnChain - ) - await tx.wait(1) - setTransactionStatus(transactionType, TransactionStatus.Succeeded) - } catch (error: any) { - setTransactionStatus( - transactionType, - isWalletRejectionError(error) - ? TransactionStatus.Rejected - : TransactionStatus.Failed - ) - } - } - }, - [contract, account] - ) + const { setTokenLoading, setTokenBalance, setTokenBalanceError } = + useTokenState() const balanceOf = useCallback( - async (token: Token) => { - if (account) { - try { - setTokenLoading(token, true) - const balance = await contract?.balanceOf(account as string) - setTokenBalance(token, balance.toString()) - setTokenLoading(token, false) - } catch (error) { - setTokenLoading(Token.Nu, false) - console.log( - `Error: Fetching ${token} balance failed for ${account}`, - error - ) - } + async (address) => { + try { + setTokenLoading(tokenName, true) + const balance = await token.balanceOf(address) + setTokenBalance(tokenName, balance.toString()) + } catch (error) { + setTokenBalanceError(tokenName) + console.log( + `Error: Fetching ${token} balance failed for ${address}`, + error + ) } }, - [account, contract] + [token, setTokenLoading, setTokenBalanceError] ) - return { approve, balanceOf, contract } + return { balanceOf, contract: token.contract, wrapper: token } } diff --git a/src/web3/hooks/useKeep.ts b/src/web3/hooks/useKeep.ts index 91d093981..6cd9ca50a 100644 --- a/src/web3/hooks/useKeep.ts +++ b/src/web3/hooks/useKeep.ts @@ -1,36 +1,9 @@ -import KeepToken from "@keep-network/keep-core/artifacts/KeepToken.json" import { useErc20TokenContract } from "./useERC20" import { Token } from "../../enums" -import { TransactionType } from "../../enums/transactionType" -import { Contract } from "@ethersproject/contracts" -import { getContractAddressFromTruffleArtifact } from "../../utils/getContract" +import { useThreshold } from "../../contexts/ThresholdContext" -export interface UseKeep { - (): { - approveKeep: () => void - fetchKeepBalance: () => void - contract: Contract | null - } -} - -export const useKeep: UseKeep = () => { - const { balanceOf, approve, contract } = useErc20TokenContract( - getContractAddressFromTruffleArtifact(KeepToken), - undefined, - KeepToken.abi - ) - - const approveKeep = () => { - approve(TransactionType.ApproveKeep) - } - - const fetchKeepBalance = () => { - balanceOf(Token.Keep) - } +export const useKeep = () => { + const threshold = useThreshold() - return { - approveKeep, - fetchKeepBalance, - contract, - } + return useErc20TokenContract(threshold.tokens.keep, Token.Keep) } diff --git a/src/web3/hooks/useNu.ts b/src/web3/hooks/useNu.ts index b902a337f..2a865cc11 100644 --- a/src/web3/hooks/useNu.ts +++ b/src/web3/hooks/useNu.ts @@ -1,35 +1,8 @@ -import NuCypherToken from "@threshold-network/solidity-contracts/artifacts/NuCypherToken.json" -import { Contract } from "@ethersproject/contracts" import { useErc20TokenContract } from "./useERC20" import { Token } from "../../enums" -import { TransactionType } from "../../enums/transactionType" +import { useThreshold } from "../../contexts/ThresholdContext" -export interface UseNu { - (): { - approveNu: () => void - fetchNuBalance: () => void - contract: Contract | null - } -} - -export const useNu: UseNu = () => { - const { balanceOf, approve, contract } = useErc20TokenContract( - NuCypherToken.address, - undefined, - NuCypherToken.abi - ) - - const approveNu = () => { - approve(TransactionType.ApproveNu) - } - - const fetchNuBalance = () => { - balanceOf(Token.Nu) - } - - return { - fetchNuBalance, - approveNu, - contract, - } +export const useNu = () => { + const threshold = useThreshold() + return useErc20TokenContract(threshold.tokens.nu, Token.Nu) } diff --git a/src/web3/hooks/useT.ts b/src/web3/hooks/useT.ts index 68907448d..153cb5dc6 100644 --- a/src/web3/hooks/useT.ts +++ b/src/web3/hooks/useT.ts @@ -1,34 +1,8 @@ -import T from "@threshold-network/solidity-contracts/artifacts/T.json" -import { Contract } from "@ethersproject/contracts" import { useErc20TokenContract } from "./useERC20" -import { Token, TransactionType } from "../../enums" +import { Token } from "../../enums" +import { useThreshold } from "../../contexts/ThresholdContext" -export interface UseT { - (): { - approveT: () => void - fetchTBalance: () => void - contract: Contract | null - } -} - -export const useT: UseT = () => { - const { balanceOf, approve, contract } = useErc20TokenContract( - T.address, - undefined, - T.abi - ) - - const approveT = () => { - approve(TransactionType.ApproveT) - } - - const fetchTBalance = () => { - balanceOf(Token.T) - } - - return { - approveT, - fetchTBalance, - contract, - } +export const useT = () => { + const threshold = useThreshold() + return useErc20TokenContract(threshold.tokens.t, Token.T) } diff --git a/src/web3/hooks/useTBTCTokenContract.ts b/src/web3/hooks/useTBTCTokenContract.ts index 1900fd6c7..3c0d9cfbd 100644 --- a/src/web3/hooks/useTBTCTokenContract.ts +++ b/src/web3/hooks/useTBTCTokenContract.ts @@ -1,7 +1,8 @@ -import TBTCToken from "@keep-network/tbtc/artifacts/TBTCToken.json" import { useErc20TokenContract } from "./useERC20" -import { getContractAddressFromTruffleArtifact } from "../../utils/getContract" +import { useThreshold } from "../../contexts/ThresholdContext" +import { Token } from "../../enums" export const useTBTCTokenContract = () => { - return useErc20TokenContract(getContractAddressFromTruffleArtifact(TBTCToken)) + const threshold = useThreshold() + return useErc20TokenContract(threshold.tokens.tbtcv1, Token.TBTC) } diff --git a/src/web3/hooks/useTBTCv2TokenContract.ts b/src/web3/hooks/useTBTCv2TokenContract.ts index 5235886ec..0ee6d92a8 100644 --- a/src/web3/hooks/useTBTCv2TokenContract.ts +++ b/src/web3/hooks/useTBTCv2TokenContract.ts @@ -1,29 +1,8 @@ import { useErc20TokenContract } from "./useERC20" -import { Token, TransactionType } from "../../enums" -import TBTC from "@keep-network/tbtc-v2/artifacts/TBTC.json" -import { featureFlags } from "../../constants" +import { Token } from "../../enums" +import { useThreshold } from "../../contexts/ThresholdContext" -export const useTBTCv2TokenContract: any = () => { - if (featureFlags.TBTC_V2) { - const { balanceOf, approve, contract } = useErc20TokenContract( - TBTC.address, - undefined, - TBTC.abi - ) - - // TODO: - const approveTBTCV2 = () => {} - - const fetchTBTCV2Balance = () => { - balanceOf(Token.TBTCV2) - } - - return { - fetchTBTCV2Balance, - approveTBTCV2, - contract, - } - } - - return undefined +export const useTBTCv2TokenContract = () => { + const threshold = useThreshold() + return useErc20TokenContract(threshold.tokens.tbtc, Token.TBTCV2) } From 4f7cf47fee9f22ad17a492f6f339ce59e4786349 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 9 Nov 2022 11:54:21 +0100 Subject: [PATCH 04/10] Remove the Token Context Provider This context provider is now unnecessary. We moved fetching token balances to redux listener middleware and we can get the token state from redux so there is no need for additional context provider. --- src/App.tsx | 9 ++-- src/contexts/TokenContext.tsx | 80 ---------------------------------- src/hooks/useToken.ts | 37 ++++++++++++++-- src/store/tokens/selectors.ts | 13 ++++++ src/store/tokens/tokenSlice.ts | 4 +- src/web3/hooks/index.ts | 1 + src/web3/hooks/useERC20.ts | 2 +- 7 files changed, 54 insertions(+), 92 deletions(-) delete mode 100644 src/contexts/TokenContext.tsx create mode 100644 src/store/tokens/selectors.ts diff --git a/src/App.tsx b/src/App.tsx index 47ebfbea6..cd8144326 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,7 +17,6 @@ import { } from "react-router-dom" import { BigNumberish } from "@ethersproject/bignumber" import { Event } from "@ethersproject/contracts" -import { TokenContextProvider } from "./contexts/TokenContext" import theme from "./theme" import reduxStore, { resetStoreAction } from "./store" import ModalRoot from "./components/Modal" @@ -204,11 +203,9 @@ const App: FC = () => { - - - - - + + + diff --git a/src/contexts/TokenContext.tsx b/src/contexts/TokenContext.tsx deleted file mode 100644 index 3556d5143..000000000 --- a/src/contexts/TokenContext.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { createContext } from "react" -import { Contract } from "@ethersproject/contracts" -import { useWeb3React } from "@web3-react/core" -import { useKeep } from "../web3/hooks/useKeep" -import { useNu } from "../web3/hooks/useNu" -import { useT } from "../web3/hooks/useT" -import { useTokenState } from "../hooks/useTokenState" -import { Token } from "../enums" -import { TokenState } from "../types" -import { useTBTCTokenContract } from "../web3/hooks" -import { useFetchOwnerStakes } from "../hooks/useFetchOwnerStakes" -import { useTBTCv2TokenContract } from "../web3/hooks/useTBTCv2TokenContract" - -interface TokenContextState extends TokenState { - contract: Contract | null -} - -export const TokenContext = createContext<{ - [key in Token]: TokenContextState -}>({ - [Token.Keep]: {} as TokenContextState, - [Token.Nu]: {} as TokenContextState, - [Token.T]: {} as TokenContextState, - [Token.TBTC]: {} as TokenContextState, - [Token.TBTCV2]: {} as TokenContextState, -}) - -// Context that handles data fetching when a user connects their wallet or -// switches their network -export const TokenContextProvider: React.FC = ({ children }) => { - const keep = useKeep() - const nu = useNu() - const t = useT() - const tbtc = useTBTCTokenContract() - const tbtcv2 = useTBTCv2TokenContract() - const { account } = useWeb3React() - const fetchOwnerStakes = useFetchOwnerStakes() - - const { - keep: keepData, - nu: nuData, - t: tData, - tbtc: tbtcData, - tbtcv2: tbtcv2Data, - } = useTokenState() - - // fetch user stakes when they connect their wallet - React.useEffect(() => { - fetchOwnerStakes(account!) - }, [fetchOwnerStakes, account]) - - return ( - - {children} - - ) -} diff --git a/src/hooks/useToken.ts b/src/hooks/useToken.ts index be6a222ed..4d7e0ab8c 100644 --- a/src/hooks/useToken.ts +++ b/src/hooks/useToken.ts @@ -1,9 +1,38 @@ -import { useContext } from "react" -import { TokenContext } from "../contexts/TokenContext" import { Token } from "../enums" +import { selectTokenByTokenName } from "../store/tokens/selectors" +import { + useKeep, + useNu, + useT, + useTBTCTokenContract, + useTBTCv2TokenContract, +} from "../web3/hooks" +import { useAppSelector } from "./store" + +const useSupportedTokens = () => { + const keep = useKeep() + const nu = useNu() + const t = useT() + const tbtc = useTBTCTokenContract() + const tbtcv2 = useTBTCv2TokenContract() + + return { + [Token.Keep]: keep, + [Token.Nu]: nu, + [Token.T]: t, + [Token.TBTC]: tbtc, + [Token.TBTCV2]: tbtcv2, + } +} export const useToken = (token: Token) => { - const tokenContext = useContext(TokenContext) + const tokenState = useAppSelector((state) => + selectTokenByTokenName(state, token) + ) + const _token = useSupportedTokens()[token] - return tokenContext[token] + return { + ...tokenState, + ..._token, + } } diff --git a/src/store/tokens/selectors.ts b/src/store/tokens/selectors.ts new file mode 100644 index 000000000..78792d38e --- /dev/null +++ b/src/store/tokens/selectors.ts @@ -0,0 +1,13 @@ +import { createSelector } from "@reduxjs/toolkit" +import { RootState } from ".." +import { TokensState } from "./tokenSlice" +import { Token } from "../../enums" + +export const selectTokensState = (state: RootState) => state.token + +export const selectTokenByTokenName = createSelector( + [selectTokensState, (_: RootState, tokenName: Token) => tokenName], + (tokensState: TokensState, tokenName: Token) => { + return tokensState[tokenName] + } +) diff --git a/src/store/tokens/tokenSlice.ts b/src/store/tokens/tokenSlice.ts index b84c38a66..7f1f5a515 100644 --- a/src/store/tokens/tokenSlice.ts +++ b/src/store/tokens/tokenSlice.ts @@ -23,6 +23,8 @@ export const fetchTokenPriceUSD = createAsyncThunk( } ) +export type TokensState = Record + export const tokenSlice = createSlice({ name: "tokens", initialState: { @@ -62,7 +64,7 @@ export const tokenSlice = createSlice({ usdConversion: 0, usdBalance: "0", }, - } as Record, + } as TokensState, reducers: { setTokenLoading: ( state, diff --git a/src/web3/hooks/index.ts b/src/web3/hooks/index.ts index c18ab75d6..b0387739b 100644 --- a/src/web3/hooks/index.ts +++ b/src/web3/hooks/index.ts @@ -18,3 +18,4 @@ export * from "./useTStakingContract" export * from "./useKeepTokenStakingContract" export * from "./usePREContract" export * from "./useClaimMerkleRewardsTransaction" +export * from "./useTBTCv2TokenContract" diff --git a/src/web3/hooks/useERC20.ts b/src/web3/hooks/useERC20.ts index 4d689a22b..1643a3bfa 100644 --- a/src/web3/hooks/useERC20.ts +++ b/src/web3/hooks/useERC20.ts @@ -28,7 +28,7 @@ export const useErc20TokenContract: UseErc20Interface = ( ) } }, - [token, setTokenLoading, setTokenBalanceError] + [token, tokenName, setTokenLoading, setTokenBalanceError] ) return { balanceOf, contract: token.contract, wrapper: token } From 4220a1b305fa6cc5c1c6a327df7cf35f69d4a36d Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Mon, 28 Nov 2022 13:58:29 +0100 Subject: [PATCH 05/10] Use `async/await` in ERC20 class methods --- src/threshold-ts/tokens/erc20/index.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/threshold-ts/tokens/erc20/index.ts b/src/threshold-ts/tokens/erc20/index.ts index 48c03d2d5..995d0b2a1 100644 --- a/src/threshold-ts/tokens/erc20/index.ts +++ b/src/threshold-ts/tokens/erc20/index.ts @@ -29,20 +29,24 @@ export class BaseERC20Token implements IERC20 { config.account ) } - balanceOf = (account: string): Promise => { - return this._contract.balanceOf(account) + + balanceOf = async (account: string): Promise => { + return await this._contract.balanceOf(account) } - allowance = (owner: string, spender: string): Promise => { - return this._contract.allowance(owner, spender) + allowance = async (owner: string, spender: string): Promise => { + return await this._contract.allowance(owner, spender) } - totalSupply = (): Promise => { - return this._contract.totalSupply() + totalSupply = async (): Promise => { + return await this._contract.totalSupply() } - approve = (spender: string, amount: string): Promise => { - return this._contract.approve(spender, amount) + approve = async ( + spender: string, + amount: string + ): Promise => { + return await this._contract.approve(spender, amount) } get contract() { From 9973b5b0264c27d6c722dc9a94fbf4b123bc3a8b Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Mon, 28 Nov 2022 14:00:09 +0100 Subject: [PATCH 06/10] Rename wrapper for all tokens in Threshold lib We agree that we prefer to keep it as singular instead of plural so we can call specific token as: `threshold.token.tbtc` instead of `threshold.tokens.tbtc`. --- src/store/tokens/effects.ts | 2 +- src/threshold-ts/index.ts | 4 ++-- src/web3/hooks/useKeep.ts | 2 +- src/web3/hooks/useNu.ts | 2 +- src/web3/hooks/useT.ts | 2 +- src/web3/hooks/useTBTCTokenContract.ts | 2 +- src/web3/hooks/useTBTCv2TokenContract.ts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/store/tokens/effects.ts b/src/store/tokens/effects.ts index 5b4fb2fd6..469fca0a6 100644 --- a/src/store/tokens/effects.ts +++ b/src/store/tokens/effects.ts @@ -18,7 +18,7 @@ export const fetchTokenBalances = async ( const address = actionCreator.payload if (!isAddress(address)) return - const { keep, nu, t, tbtc, tbtcv1 } = listenerApi.extra.threshold.tokens + const { keep, nu, t, tbtc, tbtcv1 } = listenerApi.extra.threshold.token const tokens = [ { token: keep, name: Token.Keep }, diff --git a/src/threshold-ts/index.ts b/src/threshold-ts/index.ts index e4db426d2..ec74edd9d 100644 --- a/src/threshold-ts/index.ts +++ b/src/threshold-ts/index.ts @@ -10,7 +10,7 @@ export class Threshold { staking!: IStaking multiAppStaking!: MultiAppStaking vendingMachines!: IVendingMachines - tokens!: ITokens + token!: ITokens constructor(config: ThresholdConfig) { this._initialize(config) @@ -18,7 +18,7 @@ export class Threshold { private _initialize = (config: ThresholdConfig) => { this.multicall = new Multicall(config.ethereum) - this.tokens = new Tokens(config.ethereum) + this.token = new Tokens(config.ethereum) this.vendingMachines = new VendingMachines(config.ethereum) this.staking = new Staking( config.ethereum, diff --git a/src/web3/hooks/useKeep.ts b/src/web3/hooks/useKeep.ts index 6cd9ca50a..a3879cb6c 100644 --- a/src/web3/hooks/useKeep.ts +++ b/src/web3/hooks/useKeep.ts @@ -5,5 +5,5 @@ import { useThreshold } from "../../contexts/ThresholdContext" export const useKeep = () => { const threshold = useThreshold() - return useErc20TokenContract(threshold.tokens.keep, Token.Keep) + return useErc20TokenContract(threshold.token.keep, Token.Keep) } diff --git a/src/web3/hooks/useNu.ts b/src/web3/hooks/useNu.ts index 2a865cc11..07d69ea9a 100644 --- a/src/web3/hooks/useNu.ts +++ b/src/web3/hooks/useNu.ts @@ -4,5 +4,5 @@ import { useThreshold } from "../../contexts/ThresholdContext" export const useNu = () => { const threshold = useThreshold() - return useErc20TokenContract(threshold.tokens.nu, Token.Nu) + return useErc20TokenContract(threshold.token.nu, Token.Nu) } diff --git a/src/web3/hooks/useT.ts b/src/web3/hooks/useT.ts index 153cb5dc6..b22795688 100644 --- a/src/web3/hooks/useT.ts +++ b/src/web3/hooks/useT.ts @@ -4,5 +4,5 @@ import { useThreshold } from "../../contexts/ThresholdContext" export const useT = () => { const threshold = useThreshold() - return useErc20TokenContract(threshold.tokens.t, Token.T) + return useErc20TokenContract(threshold.token.t, Token.T) } diff --git a/src/web3/hooks/useTBTCTokenContract.ts b/src/web3/hooks/useTBTCTokenContract.ts index 3c0d9cfbd..bc488e6f3 100644 --- a/src/web3/hooks/useTBTCTokenContract.ts +++ b/src/web3/hooks/useTBTCTokenContract.ts @@ -4,5 +4,5 @@ import { Token } from "../../enums" export const useTBTCTokenContract = () => { const threshold = useThreshold() - return useErc20TokenContract(threshold.tokens.tbtcv1, Token.TBTC) + return useErc20TokenContract(threshold.token.tbtcv1, Token.TBTC) } diff --git a/src/web3/hooks/useTBTCv2TokenContract.ts b/src/web3/hooks/useTBTCv2TokenContract.ts index 0ee6d92a8..210531739 100644 --- a/src/web3/hooks/useTBTCv2TokenContract.ts +++ b/src/web3/hooks/useTBTCv2TokenContract.ts @@ -4,5 +4,5 @@ import { useThreshold } from "../../contexts/ThresholdContext" export const useTBTCv2TokenContract = () => { const threshold = useThreshold() - return useErc20TokenContract(threshold.tokens.tbtc, Token.TBTCV2) + return useErc20TokenContract(threshold.token.tbtc, Token.TBTCV2) } From c17cb1bf93bf3cfb9442d975b37c123393292333 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Mon, 28 Nov 2022 14:17:22 +0100 Subject: [PATCH 07/10] Change Token enums `Token.TBTC` -> `Token.TBTCV1` `Token.TBTCV2` -> `Token.TBTC` In terms of threshold staking applications, we have only tbtc v2 app so I think from code's/T dapp's perspective is ok to use tbtc w/o v-2 because it's obvious that only tbtc-v2 works on threshold network. We only added the tbtc v1 token to get the token price in usd and calculate tvl. Ref: https://github.com/threshold-network/token-dashboard/pull/178#discussion_r964684644 --- src/components/TokenBalanceCard/index.tsx | 4 ++-- src/enums/token.ts | 6 +++--- src/hooks/__tests__/useFetchTvl.test.tsx | 4 ++-- src/hooks/useFetchTvl.ts | 2 +- src/hooks/useToken.ts | 6 +++--- src/hooks/useTokenState.ts | 6 +++--- src/pages/tBTC/Bridge/TbtcBalanceCard.tsx | 2 +- src/store/tokens/effects.ts | 6 +++--- src/store/tokens/tokenSlice.ts | 4 ++-- src/web3/hooks/useTBTCTokenContract.ts | 2 +- src/web3/hooks/useTBTCv2TokenContract.ts | 2 +- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/components/TokenBalanceCard/index.tsx b/src/components/TokenBalanceCard/index.tsx index 857b49deb..921bcb394 100644 --- a/src/components/TokenBalanceCard/index.tsx +++ b/src/components/TokenBalanceCard/index.tsx @@ -8,7 +8,7 @@ import { useToken } from "../../hooks/useToken" import { tBTCFillBlack } from "../../static/icons/tBTCFillBlack" export interface TokenBalanceCardProps { - token: Exclude + token: Exclude title?: string tokenSymbol?: string withSymbol?: boolean @@ -18,7 +18,7 @@ const tokenToIconMap = { [Token.Keep]: KeepCircleBrand, [Token.Nu]: NuCircleBrand, [Token.T]: T, - [Token.TBTCV2]: tBTCFillBlack, + [Token.TBTC]: tBTCFillBlack, } const TokenBalanceCard: FC = ({ diff --git a/src/enums/token.ts b/src/enums/token.ts index 024ccdac0..53527a97a 100644 --- a/src/enums/token.ts +++ b/src/enums/token.ts @@ -2,8 +2,8 @@ export enum Token { Keep = "KEEP", Nu = "NU", T = "T", + TBTCV1 = "TBTCV1", TBTC = "TBTC", - TBTCV2 = "TBTCV2", } export enum CoingeckoID { @@ -11,9 +11,9 @@ export enum CoingeckoID { NU = "nucypher", T = "threshold-network-token", ETH = "ethereum", - TBTC = "tbtc", + TBTCV1 = "tbtc", // TODO: add prope tbtc-v2 id when it lands on coingecko - TBTCV2 = "tbtc", + TBTC = "tbtc", } export enum TConversionRates { diff --git a/src/hooks/__tests__/useFetchTvl.test.tsx b/src/hooks/__tests__/useFetchTvl.test.tsx index 1eed3f3d7..1a25797d8 100644 --- a/src/hooks/__tests__/useFetchTvl.test.tsx +++ b/src/hooks/__tests__/useFetchTvl.test.tsx @@ -60,7 +60,7 @@ describe("Test `useFetchTvl` hook", () => { { // then expect(useETHData).toHaveBeenCalled() expect(spyOnUseToken).toHaveBeenCalledWith(Token.Keep) - expect(spyOnUseToken).toHaveBeenCalledWith(Token.TBTC) + expect(spyOnUseToken).toHaveBeenCalledWith(Token.TBTCV1) expect(spyOnUseToken).toHaveBeenCalledWith(Token.T) expect(useKeepBondingContract).toHaveBeenCalled() expect(useMulticallContract).toHaveBeenCalled() diff --git a/src/hooks/useFetchTvl.ts b/src/hooks/useFetchTvl.ts index 811977b02..5dbf4d2da 100644 --- a/src/hooks/useFetchTvl.ts +++ b/src/hooks/useFetchTvl.ts @@ -51,7 +51,7 @@ export const useFetchTvl = (): [TVLData, () => Promise] => { const eth = useETHData() const keep = useToken(Token.Keep) - const tbtc = useToken(Token.TBTC) + const tbtc = useToken(Token.TBTCV1) const t = useToken(Token.T) const keepBonding = useKeepBondingContract() const multicall = useMulticallContract() diff --git a/src/hooks/useToken.ts b/src/hooks/useToken.ts index 4d7e0ab8c..3a0204ee6 100644 --- a/src/hooks/useToken.ts +++ b/src/hooks/useToken.ts @@ -13,15 +13,15 @@ const useSupportedTokens = () => { const keep = useKeep() const nu = useNu() const t = useT() - const tbtc = useTBTCTokenContract() - const tbtcv2 = useTBTCv2TokenContract() + const tbtcv1 = useTBTCTokenContract() + const tbtc = useTBTCv2TokenContract() return { [Token.Keep]: keep, [Token.Nu]: nu, [Token.T]: t, + [Token.TBTCV1]: tbtcv1, [Token.TBTC]: tbtc, - [Token.TBTCV2]: tbtcv2, } } diff --git a/src/hooks/useTokenState.ts b/src/hooks/useTokenState.ts index 585371808..c770875ea 100644 --- a/src/hooks/useTokenState.ts +++ b/src/hooks/useTokenState.ts @@ -13,8 +13,8 @@ export const useTokenState: UseTokenState = () => { const keep = useAppSelector((state) => state.token[Token.Keep]) const nu = useAppSelector((state) => state.token[Token.Nu]) const t = useAppSelector((state) => state.token[Token.T]) + const tbtcv1 = useAppSelector((state) => state.token[Token.TBTCV1]) const tbtc = useAppSelector((state) => state.token[Token.TBTC]) - const tbtcv2 = useAppSelector((state) => state.token[Token.TBTCV2]) const dispatch = useAppDispatch() @@ -43,8 +43,8 @@ export const useTokenState: UseTokenState = () => { keep, nu, t, - tbtc, - tbtcv2, + tbtc: tbtcv1, + tbtcv2: tbtc, fetchTokenPriceUSD, setTokenBalance, setTokenLoading, diff --git a/src/pages/tBTC/Bridge/TbtcBalanceCard.tsx b/src/pages/tBTC/Bridge/TbtcBalanceCard.tsx index a2af64b1c..67b566aa6 100644 --- a/src/pages/tBTC/Bridge/TbtcBalanceCard.tsx +++ b/src/pages/tBTC/Bridge/TbtcBalanceCard.tsx @@ -8,7 +8,7 @@ export const TbtcBalanceCard: FC> = ({ }) => { return ( { const threshold = useThreshold() - return useErc20TokenContract(threshold.token.tbtcv1, Token.TBTC) + return useErc20TokenContract(threshold.token.tbtcv1, Token.TBTCV1) } diff --git a/src/web3/hooks/useTBTCv2TokenContract.ts b/src/web3/hooks/useTBTCv2TokenContract.ts index 210531739..60b0b3b32 100644 --- a/src/web3/hooks/useTBTCv2TokenContract.ts +++ b/src/web3/hooks/useTBTCv2TokenContract.ts @@ -4,5 +4,5 @@ import { useThreshold } from "../../contexts/ThresholdContext" export const useTBTCv2TokenContract = () => { const threshold = useThreshold() - return useErc20TokenContract(threshold.token.tbtc, Token.TBTCV2) + return useErc20TokenContract(threshold.token.tbtc, Token.TBTC) } From e48d02740dabe22f32bf209e560f9ec19fbc98af Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Mon, 28 Nov 2022 15:10:40 +0100 Subject: [PATCH 08/10] Update token slice action names Trying to treat actions more as "describing events that occurred", rather than "setters" as recommended in official redux docs. --- src/hooks/useTokenState.ts | 9 +++++---- src/store/tokens/effects.ts | 22 ++++++++++++++++------ src/store/tokens/tokenSlice.ts | 22 ++++++++++++++++------ src/types/token.ts | 4 +++- src/web3/hooks/useERC20.ts | 3 ++- 5 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/hooks/useTokenState.ts b/src/hooks/useTokenState.ts index c770875ea..75964a769 100644 --- a/src/hooks/useTokenState.ts +++ b/src/hooks/useTokenState.ts @@ -1,8 +1,8 @@ import { - setTokenBalance as setTokenBalanceAction, - setTokenLoading as setTokenLoadingAction, + tokenBalanceFetched as setTokenBalanceAction, + tokenBalanceFetching as setTokenLoadingAction, fetchTokenPriceUSD as fetchTokenPriceAction, - setTokenBalanceError as setTokenBalanceErrorAction, + tokenBalanceFetchFailed as setTokenBalanceErrorAction, } from "../store/tokens" import { useAppDispatch, useAppSelector } from "./store" import { Token } from "../enums" @@ -35,7 +35,8 @@ export const useTokenState: UseTokenState = () => { ) const setTokenBalanceError = useCallback( - (token: Token) => dispatch(setTokenBalanceErrorAction({ token })), + (token: Token, error: string) => + dispatch(setTokenBalanceErrorAction({ token, error })), [dispatch] ) diff --git a/src/store/tokens/effects.ts b/src/store/tokens/effects.ts index 6b7d2b71b..5dc63bb6d 100644 --- a/src/store/tokens/effects.ts +++ b/src/store/tokens/effects.ts @@ -5,10 +5,10 @@ import { isAddress } from "../../web3/utils" import { walletConnected } from "../account" import { AppListenerEffectAPI } from "../listener" import { - setTokenBalance, - setTokenLoading, + tokenBalanceFetched, + tokenBalanceFetching, fetchTokenPriceUSD, - setTokenBalanceError, + tokenBalanceFetchFailed, } from "./tokenSlice" export const fetchTokenBalances = async ( @@ -36,7 +36,7 @@ export const fetchTokenBalances = async ( try { tokens.forEach((_) => { listenerApi.dispatch( - setTokenLoading({ + tokenBalanceFetching({ token: _.name, }) ) @@ -56,7 +56,10 @@ export const fetchTokenBalances = async ( tokens.forEach((_, index) => { listenerApi.dispatch( - setTokenBalance({ token: _.name, balance: balances[index].toString() }) + tokenBalanceFetched({ + token: _.name, + balance: balances[index].toString(), + }) ) }) @@ -70,7 +73,14 @@ export const fetchTokenBalances = async ( tokens .map((_) => _.name) .forEach((tokenName) => - listenerApi.dispatch(setTokenBalanceError({ token: tokenName })) + listenerApi.dispatch( + tokenBalanceFetchFailed({ + token: tokenName, + error: `Could not fetch token balances. Error: ${( + error as Error + )?.toString()}`, + }) + ) ) listenerApi.subscribe() } diff --git a/src/store/tokens/tokenSlice.ts b/src/store/tokens/tokenSlice.ts index b6f8c3d31..3cd936dde 100644 --- a/src/store/tokens/tokenSlice.ts +++ b/src/store/tokens/tokenSlice.ts @@ -35,6 +35,7 @@ export const tokenSlice = createSlice({ icon: Icon.KeepCircleBrand, usdConversion: 0, usdBalance: "0", + error: "", }, [Token.Nu]: { loading: false, @@ -43,6 +44,7 @@ export const tokenSlice = createSlice({ icon: Icon.NuCircleBrand, usdConversion: 0, usdBalance: "0", + error: "", }, [Token.T]: { loading: false, @@ -51,28 +53,31 @@ export const tokenSlice = createSlice({ icon: Icon.TCircleBrand, usdConversion: 0, usdBalance: "0", + error: "", }, [Token.TBTCV1]: { loading: false, balance: 0, usdConversion: 0, usdBalance: "0", + error: "", }, [Token.TBTC]: { loading: false, balance: 0, usdConversion: 0, usdBalance: "0", + error: "", }, } as TokensState, reducers: { - setTokenLoading: ( + tokenBalanceFetching: ( state, action: PayloadAction ) => { state[action.payload.token].loading = true }, - setTokenBalance: ( + tokenBalanceFetched: ( state, action: PayloadAction ) => { @@ -83,13 +88,15 @@ export const tokenSlice = createSlice({ state[token].balance, state[token].usdConversion ) + state[token].error = "" }, - setTokenBalanceError: ( + tokenBalanceFetchFailed: ( state, action: PayloadAction ) => { - const { token } = action.payload + const { token, error } = action.payload state[token].loading = false + state[token].error = error }, }, extraReducers: (builder) => { @@ -105,8 +112,11 @@ export const tokenSlice = createSlice({ }, }) -export const { setTokenBalance, setTokenLoading, setTokenBalanceError } = - tokenSlice.actions +export const { + tokenBalanceFetched, + tokenBalanceFetching, + tokenBalanceFetchFailed, +} = tokenSlice.actions export const registerTokensListeners = () => { startAppListening({ diff --git a/src/types/token.ts b/src/types/token.ts index d02ef38c8..b89d3b0e1 100644 --- a/src/types/token.ts +++ b/src/types/token.ts @@ -12,6 +12,7 @@ export interface TokenState { usdConversion: number usdBalance: string decimals?: number + error: string } export interface SetTokenBalanceActionPayload { @@ -21,6 +22,7 @@ export interface SetTokenBalanceActionPayload { export interface SetTokenBalanceErrorActionPayload { token: Token + error: string } export interface SetTokenLoadingActionPayload { @@ -50,7 +52,7 @@ export interface UseTokenState { ) => TokenActionTypes setTokenLoading: (token: Token, loading: boolean) => TokenActionTypes fetchTokenPriceUSD: (token: Token) => void - setTokenBalanceError: (token: Token) => TokenActionTypes + setTokenBalanceError: (token: Token, error: string) => TokenActionTypes } } diff --git a/src/web3/hooks/useERC20.ts b/src/web3/hooks/useERC20.ts index 1643a3bfa..8adadbdb5 100644 --- a/src/web3/hooks/useERC20.ts +++ b/src/web3/hooks/useERC20.ts @@ -21,7 +21,8 @@ export const useErc20TokenContract: UseErc20Interface = ( const balance = await token.balanceOf(address) setTokenBalance(tokenName, balance.toString()) } catch (error) { - setTokenBalanceError(tokenName) + const errorMessage = `Error: Fetching ${token} balance failed for ${address}` + setTokenBalanceError(tokenName, errorMessage) console.log( `Error: Fetching ${token} balance failed for ${address}`, error From 7ea246e1fb253a4ecbc5087cc50ddfd1efa837d1 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Mon, 28 Nov 2022 15:14:49 +0100 Subject: [PATCH 09/10] Add missing value to the hook deps array --- src/web3/hooks/useERC20.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web3/hooks/useERC20.ts b/src/web3/hooks/useERC20.ts index 8adadbdb5..1d352d065 100644 --- a/src/web3/hooks/useERC20.ts +++ b/src/web3/hooks/useERC20.ts @@ -29,7 +29,7 @@ export const useErc20TokenContract: UseErc20Interface = ( ) } }, - [token, tokenName, setTokenLoading, setTokenBalanceError] + [token, tokenName, setTokenLoading, setTokenBalanceError, setTokenBalance] ) return { balanceOf, contract: token.contract, wrapper: token } From 9d1f3518832ba2a0145593a6fa805092352243e6 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Mon, 28 Nov 2022 15:57:58 +0100 Subject: [PATCH 10/10] Fix `useFetchTvl` hook unit test. --- src/hooks/__tests__/useFetchTvl.test.tsx | 38 +++++++++++++----------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/hooks/__tests__/useFetchTvl.test.tsx b/src/hooks/__tests__/useFetchTvl.test.tsx index 1a25797d8..674209be2 100644 --- a/src/hooks/__tests__/useFetchTvl.test.tsx +++ b/src/hooks/__tests__/useFetchTvl.test.tsx @@ -11,8 +11,7 @@ import { } from "../../web3/hooks" import { useETHData } from "../useETHData" import { useFetchTvl } from "../useFetchTvl" -import * as useTokenModule from "../useToken" -import { TokenContext } from "../../contexts/TokenContext" +import { useToken } from "../useToken" import * as usdUtils from "../../utils/getUsdBalance" jest.mock("../../web3/hooks", () => ({ @@ -30,6 +29,11 @@ jest.mock("../useETHData", () => ({ useETHData: jest.fn(), })) +jest.mock("../useToken", () => ({ + ...(jest.requireActual("../useToken") as {}), + useToken: jest.fn(), +})) + describe("Test `useFetchTvl` hook", () => { const keepContext = { contract: {} as any, @@ -56,18 +60,7 @@ describe("Test `useFetchTvl` hook", () => { const mockedMultiCallContract = { interface: {}, address: "0x3" } const mockedKeepAssetPoolContract = { interface: {}, address: "0x4" } - const wrapper = ({ children }) => ( - - {children} - - ) + const wrapper = ({ children }) => <>{children} const multicallRequest = jest.fn() const mockedETHData = { usdPrice: 20 } @@ -88,6 +81,16 @@ describe("Test `useFetchTvl` hook", () => { ;(useKeepTokenStakingContract as jest.Mock).mockReturnValue( mockedKeepTokenStakingContract ) + ;(useToken as jest.Mock).mockImplementation((token: Token) => { + switch (token) { + case Token.Keep: + return keepContext + case Token.T: + return tContext + case Token.TBTCV1: + return tbtcContext + } + }) }) test("should fetch tvl data correctly.", async () => { @@ -110,7 +113,6 @@ describe("Test `useFetchTvl` hook", () => { const spyOnFormatUnits = jest.spyOn(ethersUnits, "formatUnits") const spyOnToUsdBalance = jest.spyOn(usdUtils, "toUsdBalance") - const spyOnUseToken = jest.spyOn(useTokenModule, "useToken") const _expectedResult = { ecdsa: ethInKeepBonding.format * mockedETHData.usdPrice, @@ -144,9 +146,9 @@ describe("Test `useFetchTvl` hook", () => { // then expect(useETHData).toHaveBeenCalled() - expect(spyOnUseToken).toHaveBeenCalledWith(Token.Keep) - expect(spyOnUseToken).toHaveBeenCalledWith(Token.TBTCV1) - expect(spyOnUseToken).toHaveBeenCalledWith(Token.T) + expect(useToken).toHaveBeenCalledWith(Token.Keep) + expect(useToken).toHaveBeenCalledWith(Token.TBTCV1) + expect(useToken).toHaveBeenCalledWith(Token.T) expect(useKeepBondingContract).toHaveBeenCalled() expect(useMulticallContract).toHaveBeenCalled() expect(useKeepAssetPoolContract).toHaveBeenCalled()