From d9fd4c4cd57a6e78ebf6af451e1a9d2cdc46b58d Mon Sep 17 00:00:00 2001 From: leifu Date: Fri, 2 Dec 2022 21:18:45 +0200 Subject: [PATCH 01/24] enable all markets on optimism goerli (#1711) --- sdk/constants/futures.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/sdk/constants/futures.ts b/sdk/constants/futures.ts index 8aa42337e8..be666391f0 100644 --- a/sdk/constants/futures.ts +++ b/sdk/constants/futures.ts @@ -25,67 +25,67 @@ export const MARKETS: Record = { [FuturesMarketKey.sLINK]: { key: FuturesMarketKey.sLINK, asset: FuturesMarketAsset.sLINK, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sSOL]: { key: FuturesMarketKey.sSOL, asset: FuturesMarketAsset.SOL, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sAVAX]: { key: FuturesMarketKey.sAVAX, asset: FuturesMarketAsset.AVAX, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sAAVE]: { key: FuturesMarketKey.sAAVE, asset: FuturesMarketAsset.AAVE, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sUNI]: { key: FuturesMarketKey.sUNI, asset: FuturesMarketAsset.UNI, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sMATIC]: { key: FuturesMarketKey.sMATIC, asset: FuturesMarketAsset.MATIC, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sXAU]: { key: FuturesMarketKey.sXAU, asset: FuturesMarketAsset.XAU, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sXAG]: { key: FuturesMarketKey.sXAG, asset: FuturesMarketAsset.XAG, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sEUR]: { key: FuturesMarketKey.sEUR, asset: FuturesMarketAsset.EUR, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sAPE]: { key: FuturesMarketKey.sAPE, asset: FuturesMarketAsset.APE, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sDYDX]: { key: FuturesMarketKey.sDYDX, asset: FuturesMarketAsset.DYDX, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sBNB]: { key: FuturesMarketKey.sBNB, asset: FuturesMarketAsset.BNB, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sDOGE]: { key: FuturesMarketKey.sDOGE, asset: FuturesMarketAsset.DOGE, - supports: 'mainnet', + supports: 'both', }, [FuturesMarketKey.sDebtRatio]: { key: FuturesMarketKey.sDebtRatio, From e4c076a81f9b211558ff3734099cb5357fad2da8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 12:21:52 -0300 Subject: [PATCH 02/24] Bump github/codeql-action from 2.1.32 to 2.1.35 (#1717) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.1.32 to 2.1.35. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4238421316c33d73aeea2801274dd286f157c2bb...b2a92eb56d8cb930006a1c6ed86b0782dd8a4297) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/audit_build_verify.yml | 2 +- .github/workflows/codeql.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/audit_build_verify.yml b/.github/workflows/audit_build_verify.yml index 346a2947a1..6a920fae2c 100644 --- a/.github/workflows/audit_build_verify.yml +++ b/.github/workflows/audit_build_verify.yml @@ -60,7 +60,7 @@ jobs: run: npm run lint:sarif - name: Upload lint results - uses: github/codeql-action/upload-sarif@4238421316c33d73aeea2801274dd286f157c2bb # pin@codeql-bundle-20220322 + uses: github/codeql-action/upload-sarif@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # pin@codeql-bundle-20220322 with: sarif_file: lint-results.sarif continue-on-error: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0068827633..7a058aebf2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,13 +21,13 @@ jobs: uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v2 - name: Initialize CodeQL - uses: github/codeql-action/init@4238421316c33d73aeea2801274dd286f157c2bb # pin@codeql-bundle-20220322 + uses: github/codeql-action/init@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # pin@codeql-bundle-20220322 with: queries: security-and-quality languages: javascript - name: Autobuild - uses: github/codeql-action/autobuild@4238421316c33d73aeea2801274dd286f157c2bb # pin@codeql-bundle-20220322 + uses: github/codeql-action/autobuild@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # pin@codeql-bundle-20220322 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4238421316c33d73aeea2801274dd286f157c2bb # pin@codeql-bundle-20220322 + uses: github/codeql-action/analyze@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # pin@codeql-bundle-20220322 From 2a09548dc4b383a1dc3d1dd3290cf9cbd5ecfd72 Mon Sep 17 00:00:00 2001 From: Halvor Holsten Strand Date: Mon, 5 Dec 2022 16:24:48 +0100 Subject: [PATCH 03/24] minor improvements (typos and links) to README. (#1716) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b5d10af343..42a9a33b4d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ The latest IPFS hash can be found under [releases](https://github.com/Kwenta/kwe ## Ethereum stack -- ethers.js v5 - Ethereum wallet implementation. +- [ethers.js v5](https://github.com/ethers-io/ethers.js) - Ethereum wallet implementation. - [Rainbowkit](https://github.com/rainbow-me/rainbowkit) - for ethereum wallet onboarding. - [@synthetixio/contracts-interface](https://github.com/Synthetixio/js-monorepo/tree/master/packages/contracts-interface) - for interactions with the Synthetix protocol. - [@synthetixio/queries](https://github.com/Synthetixio/js-monorepo/tree/master/packages/queries) - for historical data (powered by [TheGraph](https://thegraph.com/)) @@ -84,7 +84,7 @@ Page tests should be added to the \_\_tests\_\_ folder at the root as it is not ### End-2-End testing -In order to run fully automated end-2-end (e2e) tests Kwenta uses [Synpress](https://github.com/Synthetixio/synpress) (a wrapper around [Cynpress](https://www.cypress.io/)). +In order to run fully automated end-2-end (e2e) tests Kwenta uses [Synpress](https://github.com/Synthetixio/synpress) (a wrapper around [Cypress](https://www.cypress.io/)). #### Constraints @@ -127,4 +127,4 @@ Kwenta welcomes contributors. Regardless of the time you have available, everyon ## Contact -Join the community on the [Kwenta Discord server](https://discord.gg/HUPyQ63TFF)! +Join the community on the [Kwenta Discord server](https://discord.gg/kwenta)! From c908d7c0648d608ab2b0ca61ead08f186140ed9a Mon Sep 17 00:00:00 2001 From: Oluwakorede Fashokun Date: Tue, 6 Dec 2022 05:51:50 -0700 Subject: [PATCH 04/24] Move Staking UI to Redux + SDK (#1674) * Some contracts and types * Add these * Maybe work? * It actually works * Remove rewards from getClaimableRewards return * Perf things * Fix small issues * Use vestEnabled * Remove anonymous functions in EscrowTable * Restructure SDK functions * Add approve default * Switch to using BigNumber amounts * Fix types * Handle disabling buttons when action is loading * Add multicall things * Remove unused useGetFiles hook * Make wei changes * Remove unused import * Replace ethers.BigNumber with BigNumber * Small performance improvement * More small optimizations * Remove redundant async annotation * Add consideration to README * Some fixes * Disable claim when totalRewards is zero * Fix unstake for LP tokens * Fix ABI issue * Claimable rewards --- contexts/StakingContext.tsx | 15 - hooks/useStakingData.ts | 385 ------ pages/dashboard/staking.tsx | 34 +- queries/files/useGetFiles.ts | 38 - queries/staking/utils.ts | 15 +- sdk/README.md | 5 + sdk/context.ts | 20 +- sdk/contracts/abis/KwentaArrakisVault.json | 74 + sdk/contracts/abis/KwentaStakingRewards.json | 697 ++++++++++ sdk/contracts/abis/StakingRewards.json | 1065 +++++---------- sdk/contracts/abis/main.ts | 66 - sdk/contracts/constants.ts | 36 + sdk/contracts/index.ts | 101 +- sdk/contracts/types/KwentaArrakisVault.ts | 285 ++++ sdk/contracts/types/KwentaStakingRewards.ts | 1216 +++++++++++++++++ sdk/contracts/types/StakingRewards.ts | 530 +++---- .../factories/KwentaArrakisVault__factory.ts | 169 +++ .../KwentaStakingRewards__factory.ts | 725 ++++++++++ .../factories/StakingRewards__factory.ts | 325 ++--- sdk/contracts/types/factories/index.ts | 2 + sdk/contracts/types/index.ts | 4 + sdk/index.ts | 6 +- sdk/services/kwentaToken.ts | 439 ++++++ sdk/services/token.ts | 92 -- sections/dashboard/Stake/EscrowTable.tsx | 139 +- .../Stake/InputCards/EscrowInputCard.tsx | 203 ++- .../Stake/InputCards/RedeemInputCard.tsx | 104 +- .../Stake/InputCards/StakeInputCard.tsx | 171 ++- sections/dashboard/Stake/RedemptionTab.tsx | 5 +- sections/dashboard/Stake/StakingPortfolio.tsx | 93 +- sections/dashboard/Stake/StakingTab.tsx | 32 +- sections/dashboard/Stake/StakingTabs.tsx | 132 +- .../dashboard/Stake/TradingRewardsTab.tsx | 114 +- .../dashboard/Stake/VestConfirmationModal.tsx | 3 +- state/earn/actions.ts | 18 +- state/staking/actions.ts | 263 ++++ state/staking/reducer.ts | 124 ++ state/staking/selectors.ts | 132 ++ state/staking/types.ts | 31 + state/store.ts | 2 + 40 files changed, 5472 insertions(+), 2438 deletions(-) delete mode 100644 contexts/StakingContext.tsx delete mode 100644 hooks/useStakingData.ts delete mode 100644 queries/files/useGetFiles.ts create mode 100644 sdk/contracts/abis/KwentaArrakisVault.json create mode 100644 sdk/contracts/abis/KwentaStakingRewards.json create mode 100644 sdk/contracts/types/KwentaArrakisVault.ts create mode 100644 sdk/contracts/types/KwentaStakingRewards.ts create mode 100644 sdk/contracts/types/factories/KwentaArrakisVault__factory.ts create mode 100644 sdk/contracts/types/factories/KwentaStakingRewards__factory.ts create mode 100644 sdk/services/kwentaToken.ts delete mode 100644 sdk/services/token.ts create mode 100644 state/staking/actions.ts create mode 100644 state/staking/reducer.ts create mode 100644 state/staking/selectors.ts create mode 100644 state/staking/types.ts diff --git a/contexts/StakingContext.tsx b/contexts/StakingContext.tsx deleted file mode 100644 index fd079bc0f5..0000000000 --- a/contexts/StakingContext.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { createContext, useContext } from 'react'; - -import useStakingData from 'hooks/useStakingData'; - -export const StakingContext = createContext | undefined>( - undefined -); - -export const useStakingContext = () => { - const stakingContext = useContext(StakingContext); - - if (!stakingContext) throw new Error('Staking context not defined yet.'); - - return stakingContext; -}; diff --git a/hooks/useStakingData.ts b/hooks/useStakingData.ts deleted file mode 100644 index 5f0cba652b..0000000000 --- a/hooks/useStakingData.ts +++ /dev/null @@ -1,385 +0,0 @@ -import { wei } from '@synthetixio/wei'; -import { BigNumber, ethers } from 'ethers'; -import _ from 'lodash'; -import moment from 'moment'; -import { useMemo, useState } from 'react'; -import { erc20ABI, useContractRead, useContractReads, usePrepareContractWrite } from 'wagmi'; - -import { - KWENTA_TOKEN_ADDRESS, - REWARD_ESCROW, - STAKING_REWARDS, - SUPPLY_SCHEDULE, - TRADING_REWARDS, - VKWENTA_REDEEMER, - VKWENTA_TOKEN_ADDRESS, - VEKWENTA_REDEEMER, - VEKWENTA_TOKEN_ADDRESS, -} from 'constants/address'; -import { DEFAULT_NUMBER_OF_FUTURES_FEE } from 'constants/defaults'; -import Connector from 'containers/Connector'; -import { - getEpochDetails, - STAKING_HIGH_GAS_LIMIT, - STAKING_LOW_GAS_LIMIT, -} from 'queries/staking/utils'; -import multipleMerkleDistributorABI from 'sdk/contracts/abis/MultipleMerkleDistributor.json'; -import rewardEscrowABI from 'sdk/contracts/abis/RewardEscrow.json'; -import stakingRewardsABI from 'sdk/contracts/abis/StakingRewards.json'; -import supplyScheduleABI from 'sdk/contracts/abis/SupplySchedule.json'; -import veKwentaRedeemerABI from 'sdk/contracts/abis/veKwentaRedeemer.json'; -import vKwentaRedeemerABI from 'sdk/contracts/abis/vKwentaRedeemer.json'; -import { formatTruncatedDuration } from 'utils/formatters/date'; -import { zeroBN } from 'utils/formatters/number'; -import logError from 'utils/logError'; - -import useIsL2 from './useIsL2'; - -export type EscrowRow = { - id: number; - date: string; - time: string; - vestable: number; - amount: number; - fee: number; - status: 'VESTED' | 'VESTING'; -}; - -type VestingEntry = { - endTime: number; - escrowAmount: BigNumber; - entryID: number; -}; - -type VestingClaimable = { - quantity: BigNumber; - fee: BigNumber; -}; - -const useStakingData = () => { - const { network } = Connector.useContainer(); - const isL2 = useIsL2(); - const kwentaTokenContract = { - address: KWENTA_TOKEN_ADDRESS[network?.id], - abi: erc20ABI, - }; - - const stakingRewardsContract = { - address: STAKING_REWARDS[network?.id], - abi: stakingRewardsABI, - }; - - const rewardEscrowContract = useMemo(() => { - return { - address: REWARD_ESCROW[network?.id], - abi: rewardEscrowABI, - }; - }, [network?.id]); - - const supplyScheduleContract = { - address: SUPPLY_SCHEDULE[network?.id], - abi: supplyScheduleABI, - }; - - const vKwentaTokenContract = { - address: VKWENTA_TOKEN_ADDRESS[network?.id], - abi: erc20ABI, - }; - - const vKwentaRedeemerContract = { - address: VKWENTA_REDEEMER[network?.id], - abi: vKwentaRedeemerABI, - }; - - const veKwentaTokenContract = { - address: VEKWENTA_TOKEN_ADDRESS[network?.id], - abi: erc20ABI, - }; - - const veKwentaRedeemerContract = { - address: VEKWENTA_REDEEMER[network?.id], - abi: veKwentaRedeemerABI, - }; - - const multipleMerkleDistributorContract = { - address: TRADING_REWARDS[network?.id], - abi: multipleMerkleDistributorABI, - }; - - const [epochPeriod, setEpochPeriod] = useState(0); - const [weekCounter, setWeekCounter] = useState(1); - const { walletAddress } = Connector.useContainer(); - const [kwentaBalance, setKwentaBalance] = useState(zeroBN); - const [escrowedBalance, setEscrowedBalance] = useState(zeroBN); - const [stakedNonEscrowedBalance, setStakedNonEscrowedBalance] = useState(zeroBN); - const [stakedEscrowedBalance, setStakedEscrowedBalance] = useState(zeroBN); - const [totalStakedBalance, setTotalStakedBalance] = useState(zeroBN); - const [claimableBalance, setClaimableBalance] = useState(zeroBN); - const [vKwentaBalance, setVKwentaBalance] = useState(zeroBN); - const [vKwentaAllowance, setVKwentaAllowance] = useState(zeroBN); - const [kwentaAllowance, setKwentaAllowance] = useState(zeroBN); - const [veKwentaBalance, setVEKwentaBalance] = useState(zeroBN); - const [veKwentaAllowance, setVEKwentaAllowance] = useState(zeroBN); - const [totalVestable, setTotalVestable] = useState(0); - - useContractReads({ - scopeKey: 'staking', - contracts: [ - { - ...rewardEscrowContract, - functionName: 'balanceOf', - args: [walletAddress ?? undefined], - }, - { - ...stakingRewardsContract, - functionName: 'nonEscrowedBalanceOf', - args: [walletAddress ?? undefined], - }, - { - ...stakingRewardsContract, - functionName: 'escrowedBalanceOf', - args: [walletAddress ?? undefined], - }, - { - ...stakingRewardsContract, - functionName: 'earned', - args: [walletAddress ?? undefined], - }, - { - ...kwentaTokenContract, - functionName: 'balanceOf', - args: [walletAddress!], - }, - { - ...supplyScheduleContract, - functionName: 'weekCounter', - }, - { - ...stakingRewardsContract, - functionName: 'totalSupply', - }, - { - ...vKwentaTokenContract, - functionName: 'balanceOf', - args: [walletAddress!], - }, - { - ...vKwentaTokenContract, - functionName: 'allowance', - args: [walletAddress!, vKwentaRedeemerContract.address], - }, - { - ...kwentaTokenContract, - functionName: 'allowance', - args: [walletAddress!, stakingRewardsContract.address], - }, - { - ...multipleMerkleDistributorContract, - functionName: 'distributionEpoch', - }, - { - ...veKwentaTokenContract, - functionName: 'balanceOf', - args: [walletAddress!], - }, - { - ...veKwentaTokenContract, - functionName: 'allowance', - args: [walletAddress!, veKwentaRedeemerContract.address], - }, - ], - watch: true, - enabled: !!walletAddress && isL2, - allowFailure: true, - onSettled(data, error) { - if (error) logError(error); - if (data) { - setEscrowedBalance(wei(data[0] ?? zeroBN)); - setStakedNonEscrowedBalance(wei(data[1] ?? zeroBN)); - setStakedEscrowedBalance(wei(data[2] ?? zeroBN)); - setClaimableBalance(wei(data[3] ?? zeroBN)); - setKwentaBalance(wei(data[4] ?? zeroBN)); - setWeekCounter(Number(data[5] ?? 1) ?? 1); - setTotalStakedBalance(wei(data[6] ?? zeroBN)); - setVKwentaBalance(wei(data[7] ?? zeroBN)); - setVKwentaAllowance(wei(data[8] ?? zeroBN)); - setKwentaAllowance(wei(data[9] ?? zeroBN)); - setEpochPeriod(Number(data[10] ?? 0) ?? 0); - setVEKwentaBalance(wei(data[11] ?? zeroBN)); - setVEKwentaAllowance(wei(data[12] ?? zeroBN)); - } - }, - }); - - const periods = useMemo(() => { - let periods: number[] = []; - for (let i = 0; i <= epochPeriod; i++) { - periods.push(i); - } - return periods; - }, [epochPeriod]); - - const { data: vestingSchedules } = useContractRead({ - ...rewardEscrowContract, - functionName: 'getVestingSchedules', - args: [walletAddress ?? undefined, 0, DEFAULT_NUMBER_OF_FUTURES_FEE], - scopeKey: 'staking', - watch: true, - enabled: !!walletAddress, - select: (data) => (data as VestingEntry[]).filter((d) => d.escrowAmount.gt(0)), - }); - - const escrowRows: EscrowRow[] = useMemo(() => { - if (vestingSchedules && (vestingSchedules as VestingEntry[]).length > 0) { - return (vestingSchedules as VestingEntry[]).map((d: VestingEntry) => ({ - id: Number(d.entryID), - date: moment(Number(d.endTime) * 1000).format('MM/DD/YY'), - time: formatTruncatedDuration(d.endTime - new Date().getTime() / 1000), - vestable: - d.endTime * 1000 > Date.now() ? 0 : Number(ethers.utils.formatEther(d.escrowAmount)), - amount: Number(ethers.utils.formatEther(d.escrowAmount)), - fee: d.endTime * 1000 > Date.now() ? Number(ethers.utils.formatEther(d.escrowAmount)) : 0, - status: d.endTime * 1000 > Date.now() ? 'VESTING' : 'VESTED', - })); - } - return []; - }, [vestingSchedules]); - - const vestingEntries = useMemo(() => { - return escrowRows.map((d) => ({ - ...rewardEscrowContract, - functionName: 'getVestingEntryClaimable', - args: [walletAddress ?? undefined, d?.id], - enabled: !!walletAddress, - })); - }, [escrowRows, rewardEscrowContract, walletAddress]); - - useContractReads({ - contracts: vestingEntries, - scopeKey: 'staking', - watch: true, - enabled: !!walletAddress && vestingEntries.length > 0, - onSuccess(data) { - (data as VestingClaimable[]).forEach((d, index) => { - escrowRows[index].vestable = Number(ethers.utils.formatEther(d.quantity)) ?? 0; - escrowRows[index].fee = Number(ethers.utils.formatEther(d.fee)) ?? 0; - }); - setTotalVestable( - Object.values(escrowRows) - .map((d) => d.vestable) - .reduce((acc, curr) => acc + curr, 0) - ); - }, - }); - - const resetTime = useMemo(() => { - const { epochEnd } = getEpochDetails(network?.id, epochPeriod); - return epochEnd; - }, [epochPeriod, network?.id]); - - const kwentaTokenApproval = useMemo(() => kwentaBalance.gt(kwentaAllowance), [ - kwentaBalance, - kwentaAllowance, - ]); - - const vKwentaTokenApproval = useMemo(() => vKwentaBalance.gt(vKwentaAllowance), [ - vKwentaBalance, - vKwentaAllowance, - ]); - - const veKwentaTokenApproval = useMemo(() => veKwentaBalance.gt(veKwentaAllowance), [ - veKwentaBalance, - veKwentaAllowance, - ]); - - const { config: getRewardConfig } = usePrepareContractWrite({ - ...stakingRewardsContract, - functionName: 'getReward', - overrides: { - gasLimit: STAKING_HIGH_GAS_LIMIT, - }, - enabled: claimableBalance.gt(0), - }); - - const { config: kwentaApproveConfig } = usePrepareContractWrite({ - ...kwentaTokenContract, - functionName: 'approve', - args: [stakingRewardsContract.address, ethers.constants.MaxUint256], - enabled: !!walletAddress && kwentaTokenApproval, - }); - - const { config: vKwentaApproveConfig } = usePrepareContractWrite({ - ...vKwentaTokenContract, - functionName: 'approve', - args: [vKwentaRedeemerContract.address, ethers.constants.MaxUint256], - enabled: !!walletAddress && vKwentaTokenApproval, - }); - - const { config: veKwentaApproveConfig } = usePrepareContractWrite({ - ...veKwentaTokenContract, - functionName: 'approve', - args: [veKwentaRedeemerContract.address, ethers.constants.MaxUint256], - enabled: !!walletAddress && veKwentaTokenApproval, - }); - - const { config: vKwentaRedeemConfig } = usePrepareContractWrite({ - ...vKwentaRedeemerContract, - functionName: 'redeem', - overrides: { - gasLimit: STAKING_LOW_GAS_LIMIT, - }, - enabled: !!walletAddress && wei(vKwentaBalance).gt(0), - }); - - const { config: veKwentaRedeemConfig } = usePrepareContractWrite({ - ...veKwentaRedeemerContract, - functionName: 'redeem', - args: [walletAddress], - overrides: { - gasLimit: STAKING_HIGH_GAS_LIMIT, - }, - enabled: !!walletAddress && wei(veKwentaBalance).gt(0), - }); - - const userStakedBalance = useMemo(() => stakedEscrowedBalance.add(stakedNonEscrowedBalance), [ - stakedEscrowedBalance, - stakedNonEscrowedBalance, - ]); - - return { - weekCounter, - userStakedBalance: Number(userStakedBalance), - totalStakedBalance: Number(totalStakedBalance), - periods, - resetTime, - epochPeriod, - escrowRows, - escrowedBalance, - totalVestable, - stakedNonEscrowedBalance, - stakedEscrowedBalance, - claimableBalance, - kwentaBalance, - vKwentaBalance, - veKwentaBalance, - vKwentaAllowance, - veKwentaAllowance, - kwentaAllowance, - getRewardConfig, - kwentaApproveConfig, - vKwentaApproveConfig, - veKwentaApproveConfig, - vKwentaRedeemConfig, - veKwentaRedeemConfig, - kwentaTokenApproval, - vKwentaTokenApproval, - veKwentaTokenApproval, - stakingRewardsContract, - rewardEscrowContract, - vKwentaRedeemerContract, - veKwentaRedeemerContract, - multipleMerkleDistributorContract, - }; -}; - -export default useStakingData; diff --git a/pages/dashboard/staking.tsx b/pages/dashboard/staking.tsx index 2e7f51256d..ce2f244a4b 100644 --- a/pages/dashboard/staking.tsx +++ b/pages/dashboard/staking.tsx @@ -1,25 +1,43 @@ import Head from 'next/head'; -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { StakingContext } from 'contexts/StakingContext'; -import useStakingData from 'hooks/useStakingData'; import DashboardLayout from 'sections/dashboard/DashboardLayout'; -import StakingPortfolio from 'sections/dashboard/Stake/StakingPortfolio'; +import StakingPortfolio, { StakeTab } from 'sections/dashboard/Stake/StakingPortfolio'; +import StakingTabs from 'sections/dashboard/Stake/StakingTabs'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; +import { fetchEscrowData, fetchStakingData } from 'state/staking/actions'; type StakingComponent = React.FC & { getLayout: (page: HTMLElement) => JSX.Element }; const StakingPage: StakingComponent = () => { const { t } = useTranslation(); - const stakingData = useStakingData(); + const dispatch = useAppDispatch(); + const walletAddress = useAppSelector(({ wallet }) => wallet.walletAddress); + const [currentTab, setCurrentTab] = useState(StakeTab.Staking); + + useEffect(() => { + if (!!walletAddress) { + dispatch(fetchStakingData()); + dispatch(fetchEscrowData()); + } + }, [dispatch, walletAddress]); + + const handleChangeTab = useCallback( + (tab: StakeTab) => () => { + setCurrentTab(tab); + }, + [] + ); return ( - + <> {t('dashboard-stake.page-title')} - - + + + ); }; diff --git a/queries/files/useGetFiles.ts b/queries/files/useGetFiles.ts deleted file mode 100644 index 43e555336a..0000000000 --- a/queries/files/useGetFiles.ts +++ /dev/null @@ -1,38 +0,0 @@ -import axios from 'axios'; -import { useQuery } from 'react-query'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import useIsL2 from 'hooks/useIsL2'; - -import { FLEEK_BASE_URL, FLEEK_STORAGE_BUCKET } from './constants'; - -const useGetFiles = (periods: number[]) => { - const client = axios.create({ - baseURL: `${FLEEK_BASE_URL}/${FLEEK_STORAGE_BUCKET}/data/`, - timeout: 5000, - }); - const { network } = Connector.useContainer(); - const isL2 = useIsL2(); - const fileNames: string[] = periods - .slice(0, -1) - .map((i) => `trading-rewards-snapshots/${network.id === 420 ? `goerli-` : ''}epoch-${i}.json`); - - return useQuery( - QUERY_KEYS.Files.GetMultiple(fileNames), - async () => { - if (!isL2) return null; - let responses: any[] = []; - for (const fileName of fileNames) { - const response = await client.get(fileName); - responses.push(response.data ?? null); - } - return responses; - }, - { - enabled: isL2, - } - ); -}; - -export default useGetFiles; diff --git a/queries/staking/utils.ts b/queries/staking/utils.ts index d64aefc75a..37d638ed73 100644 --- a/queries/staking/utils.ts +++ b/queries/staking/utils.ts @@ -1,6 +1,8 @@ +import { NetworkId } from '@synthetixio/contracts-interface'; import { wei } from '@synthetixio/wei'; import { BigNumber } from 'ethers'; +import { formatShortDate, toJSTimestamp } from 'utils/formatters/date'; import { zeroBN } from 'utils/formatters/number'; export type TradingRewardProps = { @@ -56,10 +58,7 @@ export function getEpochDetails(networkId: number, epoch: number) { ? EPOCH_START[networkId] + WEEK * epoch : EPOCH_START[10]; const epochEndTime = currentEpochTime + WEEK; - return { - epochStart: currentEpochTime, - epochEnd: epochEndTime, - }; + return { epochStart: currentEpochTime, epochEnd: epochEndTime }; } export function getApy(totalStakedBalance: number, weekCounter: number) { @@ -69,3 +68,11 @@ export function getApy(totalStakedBalance: number, weekCounter: number) { ? yearlyRewards.mul(wei(STAKING_REWARDS_RATIO)).div(wei(totalStakedBalance)) : zeroBN; } + +export const parseEpochData = (index: number, networkId?: NetworkId) => { + const { epochStart, epochEnd } = getEpochDetails(networkId ?? 10, index); + const startDate = formatShortDate(new Date(toJSTimestamp(epochStart))); + const endDate = formatShortDate(new Date(toJSTimestamp(epochEnd))); + const label = `Epoch ${index}: ${startDate} - ${endDate}`; + return { period: index, start: epochStart, end: epochEnd, label }; +}; diff --git a/sdk/README.md b/sdk/README.md index b0ed69aeb0..eee31c9049 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -43,6 +43,7 @@ The following tasks are expected to be completed before the SDK can be considere - [ ] Set up foundation for retries on select methods. - [ ] Set up service for interacting with our subgraphs. - [ ] Remove Duplicated types. +- [ ] Create a standard way of passing in numeric values (particularly amounts) to the SDK. Weigh pros and cons of (`Wei`, `ethers.BigNumber` and `string`). ## Exchange @@ -62,6 +63,10 @@ The following tasks are expected to be completed before the SDK can be considere - [ ] Implement methods for fetching orders, past trades and transfers from the subgraph. - [ ] Consider experimenting with WebSockets for realtime data (again). +## Kwenta Token + +- [ ] Consider breaking up `getEarnDetails` and `getStakingData` functions. + # Design Considerations ## General diff --git a/sdk/context.ts b/sdk/context.ts index 73a20b3878..509a6e511b 100644 --- a/sdk/context.ts +++ b/sdk/context.ts @@ -3,7 +3,12 @@ import { Provider as EthCallProvider } from 'ethcall'; import { ethers } from 'ethers'; import * as sdkErrors from './common/errors'; -import { ContractsMap, getContractsByNetwork } from './contracts'; +import { + ContractsMap, + MultiCallContractsMap, + getContractsByNetwork, + getMultiCallContractsByNetwork, +} from './contracts'; export interface IContext { provider: ethers.providers.Provider; @@ -20,6 +25,7 @@ export default class Context implements IContext { private context: IContext; public multicallProvider = new EthCallProvider(); public contracts: ContractsMap; + public mutliCallContracts: MultiCallContractsMap; constructor(context: IContext) { this.context = { ...DEFAULT_CONTEXT, ...context }; @@ -32,7 +38,8 @@ export default class Context implements IContext { this.setSigner(context.signer); } - this.contracts = getContractsByNetwork(context.networkId, context.provider); + this.contracts = getContractsByNetwork(context.networkId, context.signer ?? context.provider); + this.mutliCallContracts = getMultiCallContractsByNetwork(context.networkId); } get networkId() { @@ -63,6 +70,10 @@ export default class Context implements IContext { return [10, 420].includes(this.networkId); } + get isMainnet() { + return [1, 10].includes(this.networkId); + } + public async setProvider(provider: ethers.providers.Provider) { this.context.provider = provider; this.multicallProvider.init(provider); @@ -74,11 +85,14 @@ export default class Context implements IContext { public setNetworkId(networkId: NetworkId) { this.context.networkId = networkId; - this.contracts = getContractsByNetwork(networkId, this.provider); + this.contracts = getContractsByNetwork(networkId, this.context.signer ?? this.provider); + this.mutliCallContracts = getMultiCallContractsByNetwork(networkId); } public async setSigner(signer: ethers.Signer) { this.context.walletAddress = await signer.getAddress(); this.context.signer = signer; + // Reinit contracts with signer when connected + this.contracts = getContractsByNetwork(this.networkId, signer); } } diff --git a/sdk/contracts/abis/KwentaArrakisVault.json b/sdk/contracts/abis/KwentaArrakisVault.json new file mode 100644 index 0000000000..aef18b9d2a --- /dev/null +++ b/sdk/contracts/abis/KwentaArrakisVault.json @@ -0,0 +1,74 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "implementationAddress", "type": "address" }, + { "internalType": "address", "name": "adminAddress", "type": "address" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "stateMutability": "payable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "previousAdmin", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "ProxyAdminTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousImplementation", + "type": "address" + }, + { "indexed": true, "internalType": "address", "name": "newImplementation", "type": "address" } + ], + "name": "ProxyImplementationUpdated", + "type": "event" + }, + { "stateMutability": "payable", "type": "fallback" }, + { + "inputs": [], + "name": "proxyAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes4", "name": "id", "type": "bytes4" }], + "name": "supportsInterface", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newAdmin", "type": "address" }], + "name": "transferProxyAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newImplementation", "type": "address" }], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newImplementation", "type": "address" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { "stateMutability": "payable", "type": "receive" } +] diff --git a/sdk/contracts/abis/KwentaStakingRewards.json b/sdk/contracts/abis/KwentaStakingRewards.json new file mode 100644 index 0000000000..3847b527c2 --- /dev/null +++ b/sdk/contracts/abis/KwentaStakingRewards.json @@ -0,0 +1,697 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_rewardEscrow", + "type": "address" + }, + { + "internalType": "address", + "name": "_supplySchedule", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "EscrowStaked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "EscrowUnstaked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldOwner", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnerChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnerNominated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Recovered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "reward", + "type": "uint256" + } + ], + "name": "RewardAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reward", + "type": "uint256" + } + ], + "name": "RewardPaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newDuration", + "type": "uint256" + } + ], + "name": "RewardsDurationUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Unstaked", + "type": "event" + }, + { + "inputs": [], + "name": "_totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "earned", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "escrowedBalanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "exit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getReward", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getRewardForDuration", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastTimeRewardApplicable", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastUpdateTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "name": "nominateNewOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "nominatedOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "nonEscrowedBalanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "reward", + "type": "uint256" + } + ], + "name": "notifyRewardAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pauseStakingRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "periodFinish", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenAmount", + "type": "uint256" + } + ], + "name": "recoverERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "rewardEscrow", + "outputs": [ + { + "internalType": "contract IRewardEscrow", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerToken", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardPerTokenStored", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "rewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardsDuration", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_rewardsDuration", + "type": "uint256" + } + ], + "name": "setRewardsDuration", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "stake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "stakeEscrow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "supplySchedule", + "outputs": [ + { + "internalType": "contract ISupplySchedule", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpauseStakingRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "unstake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "unstakeEscrow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "userRewardPerTokenPaid", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/sdk/contracts/abis/StakingRewards.json b/sdk/contracts/abis/StakingRewards.json index 3847b527c2..b679db5493 100644 --- a/sdk/contracts/abis/StakingRewards.json +++ b/sdk/contracts/abis/StakingRewards.json @@ -1,697 +1,370 @@ [ - { - "inputs": [ - { - "internalType": "address", - "name": "_token", - "type": "address" - }, - { - "internalType": "address", - "name": "_rewardEscrow", - "type": "address" - }, - { - "internalType": "address", - "name": "_supplySchedule", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "EscrowStaked", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "EscrowUnstaked", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "oldOwner", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnerChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnerNominated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Paused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "token", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "Recovered", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "reward", - "type": "uint256" - } - ], - "name": "RewardAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "reward", - "type": "uint256" - } - ], - "name": "RewardPaid", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newDuration", - "type": "uint256" - } - ], - "name": "RewardsDurationUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "Staked", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Unpaused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "Unstaked", - "type": "event" - }, - { - "inputs": [], - "name": "_totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "acceptOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "earned", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "escrowedBalanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "exit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "getReward", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "getRewardForDuration", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "lastTimeRewardApplicable", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "lastUpdateTime", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - } - ], - "name": "nominateNewOwner", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "nominatedOwner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "nonEscrowedBalanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "reward", - "type": "uint256" - } - ], - "name": "notifyRewardAmount", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pauseStakingRewards", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "paused", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "periodFinish", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "tokenAddress", - "type": "address" - }, - { - "internalType": "uint256", - "name": "tokenAmount", - "type": "uint256" - } - ], - "name": "recoverERC20", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "rewardEscrow", - "outputs": [ - { - "internalType": "contract IRewardEscrow", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "rewardPerToken", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "rewardPerTokenStored", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "rewardRate", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "name": "rewards", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "rewardsDuration", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_rewardsDuration", - "type": "uint256" - } - ], - "name": "setRewardsDuration", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "stake", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "stakeEscrow", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "supplySchedule", - "outputs": [ - { - "internalType": "contract ISupplySchedule", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "token", - "outputs": [ - { - "internalType": "contract IERC20", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "unpauseStakingRewards", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "unstake", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "unstakeEscrow", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "name": "userRewardPerTokenPaid", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - } -] \ No newline at end of file + { + "inputs": [ + { "internalType": "address", "name": "_owner", "type": "address" }, + { "internalType": "address", "name": "_rewardsDistribution", "type": "address" }, + { "internalType": "address", "name": "_rewardsToken", "type": "address" }, + { "internalType": "address", "name": "_stakingToken", "type": "address" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "oldOwner", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "OwnerChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "OwnerNominated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "bool", "name": "isPaused", "type": "bool" }], + "name": "PauseChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "token", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "Recovered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "uint256", "name": "reward", "type": "uint256" } + ], + "name": "RewardAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "user", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "reward", "type": "uint256" } + ], + "name": "RewardPaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "uint256", "name": "newDuration", "type": "uint256" } + ], + "name": "RewardsDurationUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "user", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "user", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "constant": false, + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "earned", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "exit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "getReward", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getRewardForDuration", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "lastPauseTime", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "lastTimeRewardApplicable", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "lastUpdateTime", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "_owner", "type": "address" }], + "name": "nominateNewOwner", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "nominatedOwner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "uint256", "name": "reward", "type": "uint256" }], + "name": "notifyRewardAmount", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "paused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "periodFinish", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "tokenAddress", "type": "address" }, + { "internalType": "uint256", "name": "tokenAmount", "type": "uint256" } + ], + "name": "recoverERC20", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "rewardPerToken", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "rewardPerTokenStored", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "rewardRate", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "rewards", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "rewardsDistribution", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "rewardsDuration", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "rewardsToken", + "outputs": [{ "internalType": "contract IERC20", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "bool", "name": "_paused", "type": "bool" }], + "name": "setPaused", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "_rewardsDistribution", "type": "address" }], + "name": "setRewardsDistribution", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "uint256", "name": "_rewardsDuration", "type": "uint256" }], + "name": "setRewardsDuration", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "uint256", "name": "amount", "type": "uint256" }], + "name": "stake", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "stakingToken", + "outputs": [{ "internalType": "contract IERC20", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "userRewardPerTokenPaid", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "uint256", "name": "amount", "type": "uint256" }], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/sdk/contracts/abis/main.ts b/sdk/contracts/abis/main.ts index 8c62f558ab..772a89746c 100644 --- a/sdk/contracts/abis/main.ts +++ b/sdk/contracts/abis/main.ts @@ -1,63 +1,3 @@ -export const ExchangerABI = [ - 'function getAmountsForExchange(uint sourceAmount, bytes32 sourceCurrencyKey, bytes32 destinationCurrencyKey) external view returns (uint amountReceived, uint fee, uint exchangeFeeRate)', - 'function dynamicFeeRateForExchange(bytes32 sourceCurrencyKey, bytes32 destinationCurrencyKey) external view returns (uint feeRate, bool tooVolatile)', - 'function maxSecsLeftInWaitingPeriod(address account, bytes32 currencyKey) public view returns (uint)', - 'function settlementOwing(address account, bytes32 currencyKey) public view returns (uint reclaimAmount, uint rebateAmount, uint numEntries)', - 'function feeRateForExchange(bytes32 sourceCurrencyKey, bytes32 destinationCurrencyKey) external view returns (uint)', -]; - -export const SystemStatusABI = [ - 'function futuresMarketSuspension(bytes32 marketKey) external view returns (bool suspended, uint248 reason)', -]; - -export const ExchangeRatesABI = [ - 'function ratesForCurrencies(bytes32[] currencyKeys) external view returns (uint[])', -]; - -export const SynthUtilABI = [ - 'function synthsBalances(address account) external view returns (bytes32[], uint[], uint[])', - 'function synthsRates() external view returns (bytes32[], uint[])', -]; - -export const SystemSettingsABI = [ - 'function exchangeFeeRate(bytes32 currencyKey) external view returns (uint)', -]; - -export const SynthRedeemerABI = [ - 'function balanceOf(IERC20 synthProxy, address account) external view returns (uint balanceOfInsUSD)', - 'event SynthDeprecated(address synth, uint rateToRedeem, uint totalSynthSupply, uint supplyInsUSD)', -]; - -export const FuturesMarketDataABI = [ - 'function globals() external view returns (FuturesGlobals)', - 'function allMarketSummaries() external view returns (MarketSummary[])', - 'function positionDetailsForMarketKey(bytes32 marketKey, address account) external view returns (PositionData)', -]; - -export const FuturesMarketSettingsABI = [ - 'function maxMarketValueUSD(bytes32 _marketKey) public view returns (uint)', -]; - -export const FuturesMarketABI = [ - 'function orderFee(int sizeDelta) external view returns (uint fee, bool invalid)', - 'function canLiquidate(address account) external view returns (bool)', - 'function postTradeDetails(int sizeDelta, address sender) external view returns (uint margin, int size, uint price, uint liqPrice, uint fee, IFuturesMarketBaseTypes.Status status)', -]; - -export const SynthABI = []; - -export const SynthetixABI = [ - 'function exchangeAtomically(bytes32 sourceCurrencyKey, uint sourceAmount, bytes32 destinationCurrencyKey, bytes32 trackingCode, uint minAmount) external returns (uint amountReceived)', - 'function exchangeWithTracking(bytes32 sourceCurrencyKey, uint sourceAmount, bytes32 destinationCurrencyKey, address rewardAddress, bytes32 trackingCode) external returns (uint amountReceived)', -]; - -export const SynthSwapABI = [ - 'function swapInto(bytes32 _destSynthCurrencyKey, bytes calldata _data) external payable returns (uint)', - 'function swapOutOf(bytes32 _sourceSynthCurrencyKey, uint _sourceAmount, bytes calldata _data) external returns (uint);', - 'function uniswapSwapInto(bytes32 _destSynthCurrencyKey, address _sourceTokenAddress, uint _amount, bytes calldata _data) external payable returns (uint)', - 'function uniswapSwapOutOf(bytes32 _sourceSynthCurrencyKey, address _destTokenAddress, uint _amountOfSynth, uint _expectedAmountOfSUSDFromSwap, bytes calldata _data) external returns (uint)', -]; - export const StakingRewardsABI = [ 'function periodFinish() public view returns (uint256)', 'function stake(uint256 amount) external', @@ -68,9 +8,3 @@ export const StakingRewardsABI = [ 'function totalSupply() public view returns (uint256)', 'function getReward() public', ]; - -export const KwentaArrakisVaultABI = [ - 'function approve(address spender, uint256 amount) external returns (bool)', - 'function balanceOf(address account) external view returns (uint256)', - 'function allowance(address owner, address spender) external view returns (uint256)', -]; diff --git a/sdk/contracts/constants.ts b/sdk/contracts/constants.ts index e6fdde0962..8e68b29efb 100644 --- a/sdk/contracts/constants.ts +++ b/sdk/contracts/constants.ts @@ -66,4 +66,40 @@ export const ADDRESSES: Record> = { StakingRewards: { 10: '0x6077987e8e06c062094c33177Eb12c4A65f90B65', }, + KwentaToken: { + 10: '0x920Cf626a271321C151D027030D5d08aF699456b', + 420: '0xDA0C33402Fc1e10d18c532F0Ed9c1A6c5C9e386C', + }, + KwentaStakingRewards: { + 10: '0x6e56A5D49F775BA08041e28030bc7826b13489e0', + 420: '0x1653a3a3c4ccee0538685f1600a30df5e3ee830a', + }, + RewardEscrow: { + 10: '0x1066A8eB3d90Af0Ad3F89839b974658577e75BE2', + 420: '0xaFD87d1a62260bD5714C55a1BB4057bDc8dFA413', + }, + SupplySchedule: { + 10: '0x3e8b82326Ff5f2f10da8CEa117bD44343ccb9c26', + 420: '0x671423b2e8a99882fd14bbd07e90ae8b64a0e63a', + }, + vKwentaToken: { + 10: '0x6789D8a7a7871923Fc6430432A602879eCB6520a', + 420: '0xb897D76bC9F7efB66Fb94970371ef17998c296b6', + }, + veKwentaToken: { + 10: '0x678d8f4ba8dfe6bad51796351824dcceceaeff2b', + 420: '0x3e52b5f840eafd79394c6359e93bf3ffdae89ee4', + }, + vKwentaRedeemer: { + 10: '0x8132EE584bCD6f8Eb1bea141DB7a7AC1E72917b9', + 420: '0x03c3E61D624F279243e1c8b43eD0fCF6790D10E9', + }, + veKwentaRedeemer: { + 10: '0xc7088AC8F287539567e458C7D08C2a1470Fd25B7', + 420: '0x86ca3CEbEA60101292EEFCd5802fD6e55D647c87', + }, + TradingRewards: { + 10: '0xf486A72E8c8143ACd9F65A104A16990fDb38be14', + 420: '0x74c0A3bD10634759DC8B4CA7078C8Bf85bFE1271', + }, }; diff --git a/sdk/contracts/index.ts b/sdk/contracts/index.ts index c09a9c3f0c..6b914ee9fe 100644 --- a/sdk/contracts/index.ts +++ b/sdk/contracts/index.ts @@ -1,7 +1,17 @@ import { NetworkId } from '@synthetixio/contracts-interface'; +import { Contract as EthCallContract } from 'ethcall'; import { Contract, ethers } from 'ethers'; -import { KwentaArrakisVaultABI, StakingRewardsABI } from './abis/main'; +import ERC20ABI from '../contracts/abis/ERC20.json'; +import MultipleMerkleDistributorABI from '../contracts/abis/MultipleMerkleDistributor.json'; +import RewardEscrowABI from '../contracts/abis/RewardEscrow.json'; +import SupplyScheduleABI from '../contracts/abis/SupplySchedule.json'; +import CrossMarginBaseSettingsABI from './abis/CrossMarginBaseSettings.json'; +import ExchangeRatesABI from './abis/ExchangeRates.json'; +import FuturesMarketDataABI from './abis/FuturesMarketData.json'; +import FuturesMarketSettingsABI from './abis/FuturesMarketSettings.json'; +import KwentaStakingRewardsABI from './abis/KwentaStakingRewards.json'; +import StakingRewardsABI from './abis/StakingRewards.json'; import { ADDRESSES } from './constants'; import { CrossMarginAccountFactory__factory, @@ -10,12 +20,20 @@ import { Exchanger__factory, FuturesMarketData__factory, FuturesMarketSettings__factory, + RewardEscrow__factory, Synthetix__factory, SynthRedeemer__factory, SynthSwap__factory, SynthUtil__factory, SystemSettings__factory, SystemStatus__factory, + KwentaArrakisVault__factory, + ERC20__factory, + SupplySchedule__factory, + MultipleMerkleDistributor__factory, + KwentaStakingRewards__factory, + VKwentaRedeemer__factory, + StakingRewards__factory, } from './types'; type ContractFactory = { @@ -29,7 +47,7 @@ export type AllContractsMap = Record< export const getContractsByNetwork = ( networkId: NetworkId, - provider: ethers.providers.Provider + provider: ethers.providers.Provider | ethers.Signer ) => { return { Exchanger: ADDRESSES.Exchanger[networkId] @@ -76,13 +94,88 @@ export const getContractsByNetwork = ( : undefined, // TODO: Replace these when we move away from wagmi hooks KwentaArrakisVault: ADDRESSES.KwentaArrakisVault[networkId] - ? new Contract(ADDRESSES.KwentaArrakisVault[networkId], KwentaArrakisVaultABI, provider) + ? KwentaArrakisVault__factory.connect(ADDRESSES.KwentaArrakisVault[networkId], provider) : undefined, StakingRewards: ADDRESSES.StakingRewards[networkId] - ? new Contract(ADDRESSES.StakingRewards[networkId], StakingRewardsABI, provider) + ? StakingRewards__factory.connect(ADDRESSES.StakingRewards[networkId], provider) + : undefined, + RewardEscrow: ADDRESSES.RewardEscrow[networkId] + ? RewardEscrow__factory.connect(ADDRESSES.RewardEscrow[networkId], provider) + : undefined, + KwentaToken: ADDRESSES.KwentaToken[networkId] + ? ERC20__factory.connect(ADDRESSES.KwentaToken[networkId], provider) + : undefined, + SupplySchedule: ADDRESSES.SupplySchedule[networkId] + ? SupplySchedule__factory.connect(ADDRESSES.SupplySchedule[networkId], provider) + : undefined, + vKwentaToken: ADDRESSES.vKwentaToken[networkId] + ? ERC20__factory.connect(ADDRESSES.vKwentaToken[networkId], provider) + : undefined, + MultipleMerkleDistributor: ADDRESSES.TradingRewards[networkId] + ? MultipleMerkleDistributor__factory.connect(ADDRESSES.TradingRewards[networkId], provider) + : undefined, + veKwentaToken: ADDRESSES.veKwentaToken[networkId] + ? ERC20__factory.connect(ADDRESSES.veKwentaToken[networkId], provider) + : undefined, + KwentaStakingRewards: ADDRESSES.KwentaStakingRewards[networkId] + ? KwentaStakingRewards__factory.connect(ADDRESSES.KwentaStakingRewards[networkId], provider) + : undefined, + vKwentaRedeemer: ADDRESSES.vKwentaRedeemer[networkId] + ? VKwentaRedeemer__factory.connect(ADDRESSES.vKwentaRedeemer[networkId], provider) + : undefined, + veKwentaRedeemer: ADDRESSES.veKwentaRedeemer[networkId] + ? VKwentaRedeemer__factory.connect(ADDRESSES.veKwentaRedeemer[networkId], provider) + : undefined, + }; +}; + +export const getMultiCallContractsByNetwork = (networkId: NetworkId) => { + return { + CrossMarginBaseSettings: ADDRESSES.CrossMarginBaseSettings[networkId] + ? new EthCallContract( + ADDRESSES.CrossMarginBaseSettings[networkId], + CrossMarginBaseSettingsABI + ) + : undefined, + ExchangeRates: ADDRESSES.ExchangeRates[networkId] + ? new EthCallContract(ADDRESSES.ExchangeRates[networkId], ExchangeRatesABI) + : undefined, + FuturesMarketData: ADDRESSES.FuturesMarketData[networkId] + ? new EthCallContract(ADDRESSES.FuturesMarketData[networkId], FuturesMarketDataABI) + : undefined, + FuturesMarketSettings: ADDRESSES.FuturesMarketSettings[networkId] + ? new EthCallContract(ADDRESSES.FuturesMarketSettings[networkId], FuturesMarketSettingsABI) + : undefined, + StakingRewards: ADDRESSES.StakingRewards[networkId] + ? new EthCallContract(ADDRESSES.StakingRewards[networkId], StakingRewardsABI) + : undefined, + KwentaArrakisVault: ADDRESSES.KwentaArrakisVault[networkId] + ? new EthCallContract(ADDRESSES.KwentaArrakisVault[networkId], ERC20ABI) + : undefined, + RewardEscrow: ADDRESSES.RewardEscrow[networkId] + ? new EthCallContract(ADDRESSES.RewardEscrow[networkId], RewardEscrowABI) + : undefined, + KwentaStakingRewards: ADDRESSES.KwentaStakingRewards[networkId] + ? new EthCallContract(ADDRESSES.KwentaStakingRewards[networkId], KwentaStakingRewardsABI) + : undefined, + KwentaToken: ADDRESSES.KwentaToken[networkId] + ? new EthCallContract(ADDRESSES.KwentaToken[networkId], ERC20ABI) + : undefined, + MultipleMerkleDistributor: ADDRESSES.TradingRewards[networkId] + ? new EthCallContract(ADDRESSES.TradingRewards[networkId], MultipleMerkleDistributorABI) + : undefined, + vKwentaToken: ADDRESSES.vKwentaToken[networkId] + ? new EthCallContract(ADDRESSES.vKwentaToken[networkId], ERC20ABI) + : undefined, + veKwentaToken: ADDRESSES.veKwentaToken[networkId] + ? new EthCallContract(ADDRESSES.veKwentaToken[networkId], ERC20ABI) + : undefined, + SupplySchedule: ADDRESSES.SupplySchedule[networkId] + ? new EthCallContract(ADDRESSES.SupplySchedule[networkId], SupplyScheduleABI) : undefined, }; }; export type ContractsMap = ReturnType; +export type MultiCallContractsMap = ReturnType; export type ContractName = keyof ContractsMap; diff --git a/sdk/contracts/types/KwentaArrakisVault.ts b/sdk/contracts/types/KwentaArrakisVault.ts new file mode 100644 index 0000000000..1d6b79e24f --- /dev/null +++ b/sdk/contracts/types/KwentaArrakisVault.ts @@ -0,0 +1,285 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PayableOverrides, + PopulatedTransaction, + Signer, + utils, +} from "ethers"; +import type { + FunctionFragment, + Result, + EventFragment, +} from "@ethersproject/abi"; +import type { Listener, Provider } from "@ethersproject/providers"; +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, + PromiseOrValue, +} from "./common"; + +export interface KwentaArrakisVaultInterface extends utils.Interface { + functions: { + "proxyAdmin()": FunctionFragment; + "supportsInterface(bytes4)": FunctionFragment; + "transferProxyAdmin(address)": FunctionFragment; + "upgradeTo(address)": FunctionFragment; + "upgradeToAndCall(address,bytes)": FunctionFragment; + }; + + getFunction( + nameOrSignatureOrTopic: + | "proxyAdmin" + | "supportsInterface" + | "transferProxyAdmin" + | "upgradeTo" + | "upgradeToAndCall" + ): FunctionFragment; + + encodeFunctionData( + functionFragment: "proxyAdmin", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "supportsInterface", + values: [PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "transferProxyAdmin", + values: [PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "upgradeTo", + values: [PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "upgradeToAndCall", + values: [PromiseOrValue, PromiseOrValue] + ): string; + + decodeFunctionResult(functionFragment: "proxyAdmin", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "supportsInterface", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "transferProxyAdmin", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "upgradeTo", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "upgradeToAndCall", + data: BytesLike + ): Result; + + events: { + "ProxyAdminTransferred(address,address)": EventFragment; + "ProxyImplementationUpdated(address,address)": EventFragment; + }; + + getEvent(nameOrSignatureOrTopic: "ProxyAdminTransferred"): EventFragment; + getEvent(nameOrSignatureOrTopic: "ProxyImplementationUpdated"): EventFragment; +} + +export interface ProxyAdminTransferredEventObject { + previousAdmin: string; + newAdmin: string; +} +export type ProxyAdminTransferredEvent = TypedEvent< + [string, string], + ProxyAdminTransferredEventObject +>; + +export type ProxyAdminTransferredEventFilter = + TypedEventFilter; + +export interface ProxyImplementationUpdatedEventObject { + previousImplementation: string; + newImplementation: string; +} +export type ProxyImplementationUpdatedEvent = TypedEvent< + [string, string], + ProxyImplementationUpdatedEventObject +>; + +export type ProxyImplementationUpdatedEventFilter = + TypedEventFilter; + +export interface KwentaArrakisVault extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + interface: KwentaArrakisVaultInterface; + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>; + + listeners( + eventFilter?: TypedEventFilter + ): Array>; + listeners(eventName?: string): Array; + removeAllListeners( + eventFilter: TypedEventFilter + ): this; + removeAllListeners(eventName?: string): this; + off: OnEvent; + on: OnEvent; + once: OnEvent; + removeListener: OnEvent; + + functions: { + proxyAdmin(overrides?: CallOverrides): Promise<[string]>; + + supportsInterface( + id: PromiseOrValue, + overrides?: CallOverrides + ): Promise<[boolean]>; + + transferProxyAdmin( + newAdmin: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + upgradeTo( + newImplementation: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + upgradeToAndCall( + newImplementation: PromiseOrValue, + data: PromiseOrValue, + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise; + }; + + proxyAdmin(overrides?: CallOverrides): Promise; + + supportsInterface( + id: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + transferProxyAdmin( + newAdmin: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + upgradeTo( + newImplementation: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + upgradeToAndCall( + newImplementation: PromiseOrValue, + data: PromiseOrValue, + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise; + + callStatic: { + proxyAdmin(overrides?: CallOverrides): Promise; + + supportsInterface( + id: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + transferProxyAdmin( + newAdmin: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + upgradeTo( + newImplementation: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + upgradeToAndCall( + newImplementation: PromiseOrValue, + data: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + }; + + filters: { + "ProxyAdminTransferred(address,address)"( + previousAdmin?: PromiseOrValue | null, + newAdmin?: PromiseOrValue | null + ): ProxyAdminTransferredEventFilter; + ProxyAdminTransferred( + previousAdmin?: PromiseOrValue | null, + newAdmin?: PromiseOrValue | null + ): ProxyAdminTransferredEventFilter; + + "ProxyImplementationUpdated(address,address)"( + previousImplementation?: PromiseOrValue | null, + newImplementation?: PromiseOrValue | null + ): ProxyImplementationUpdatedEventFilter; + ProxyImplementationUpdated( + previousImplementation?: PromiseOrValue | null, + newImplementation?: PromiseOrValue | null + ): ProxyImplementationUpdatedEventFilter; + }; + + estimateGas: { + proxyAdmin(overrides?: CallOverrides): Promise; + + supportsInterface( + id: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + transferProxyAdmin( + newAdmin: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + upgradeTo( + newImplementation: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + upgradeToAndCall( + newImplementation: PromiseOrValue, + data: PromiseOrValue, + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise; + }; + + populateTransaction: { + proxyAdmin(overrides?: CallOverrides): Promise; + + supportsInterface( + id: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + transferProxyAdmin( + newAdmin: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + upgradeTo( + newImplementation: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + upgradeToAndCall( + newImplementation: PromiseOrValue, + data: PromiseOrValue, + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise; + }; +} diff --git a/sdk/contracts/types/KwentaStakingRewards.ts b/sdk/contracts/types/KwentaStakingRewards.ts new file mode 100644 index 0000000000..ac44355a01 --- /dev/null +++ b/sdk/contracts/types/KwentaStakingRewards.ts @@ -0,0 +1,1216 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PopulatedTransaction, + Signer, + utils, +} from "ethers"; +import type { + FunctionFragment, + Result, + EventFragment, +} from "@ethersproject/abi"; +import type { Listener, Provider } from "@ethersproject/providers"; +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, + PromiseOrValue, +} from "./common"; + +export interface KwentaStakingRewardsInterface extends utils.Interface { + functions: { + "_totalSupply()": FunctionFragment; + "acceptOwnership()": FunctionFragment; + "balanceOf(address)": FunctionFragment; + "earned(address)": FunctionFragment; + "escrowedBalanceOf(address)": FunctionFragment; + "exit()": FunctionFragment; + "getReward()": FunctionFragment; + "getRewardForDuration()": FunctionFragment; + "lastTimeRewardApplicable()": FunctionFragment; + "lastUpdateTime()": FunctionFragment; + "nominateNewOwner(address)": FunctionFragment; + "nominatedOwner()": FunctionFragment; + "nonEscrowedBalanceOf(address)": FunctionFragment; + "notifyRewardAmount(uint256)": FunctionFragment; + "owner()": FunctionFragment; + "pauseStakingRewards()": FunctionFragment; + "paused()": FunctionFragment; + "periodFinish()": FunctionFragment; + "recoverERC20(address,uint256)": FunctionFragment; + "rewardEscrow()": FunctionFragment; + "rewardPerToken()": FunctionFragment; + "rewardPerTokenStored()": FunctionFragment; + "rewardRate()": FunctionFragment; + "rewards(address)": FunctionFragment; + "rewardsDuration()": FunctionFragment; + "setRewardsDuration(uint256)": FunctionFragment; + "stake(uint256)": FunctionFragment; + "stakeEscrow(address,uint256)": FunctionFragment; + "supplySchedule()": FunctionFragment; + "token()": FunctionFragment; + "totalSupply()": FunctionFragment; + "unpauseStakingRewards()": FunctionFragment; + "unstake(uint256)": FunctionFragment; + "unstakeEscrow(address,uint256)": FunctionFragment; + "userRewardPerTokenPaid(address)": FunctionFragment; + }; + + getFunction( + nameOrSignatureOrTopic: + | "_totalSupply" + | "acceptOwnership" + | "balanceOf" + | "earned" + | "escrowedBalanceOf" + | "exit" + | "getReward" + | "getRewardForDuration" + | "lastTimeRewardApplicable" + | "lastUpdateTime" + | "nominateNewOwner" + | "nominatedOwner" + | "nonEscrowedBalanceOf" + | "notifyRewardAmount" + | "owner" + | "pauseStakingRewards" + | "paused" + | "periodFinish" + | "recoverERC20" + | "rewardEscrow" + | "rewardPerToken" + | "rewardPerTokenStored" + | "rewardRate" + | "rewards" + | "rewardsDuration" + | "setRewardsDuration" + | "stake" + | "stakeEscrow" + | "supplySchedule" + | "token" + | "totalSupply" + | "unpauseStakingRewards" + | "unstake" + | "unstakeEscrow" + | "userRewardPerTokenPaid" + ): FunctionFragment; + + encodeFunctionData( + functionFragment: "_totalSupply", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "acceptOwnership", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "balanceOf", + values: [PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "earned", + values: [PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "escrowedBalanceOf", + values: [PromiseOrValue] + ): string; + encodeFunctionData(functionFragment: "exit", values?: undefined): string; + encodeFunctionData(functionFragment: "getReward", values?: undefined): string; + encodeFunctionData( + functionFragment: "getRewardForDuration", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "lastTimeRewardApplicable", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "lastUpdateTime", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "nominateNewOwner", + values: [PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "nominatedOwner", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "nonEscrowedBalanceOf", + values: [PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "notifyRewardAmount", + values: [PromiseOrValue] + ): string; + encodeFunctionData(functionFragment: "owner", values?: undefined): string; + encodeFunctionData( + functionFragment: "pauseStakingRewards", + values?: undefined + ): string; + encodeFunctionData(functionFragment: "paused", values?: undefined): string; + encodeFunctionData( + functionFragment: "periodFinish", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "recoverERC20", + values: [PromiseOrValue, PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "rewardEscrow", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "rewardPerToken", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "rewardPerTokenStored", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "rewardRate", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "rewards", + values: [PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "rewardsDuration", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "setRewardsDuration", + values: [PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "stake", + values: [PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "stakeEscrow", + values: [PromiseOrValue, PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "supplySchedule", + values?: undefined + ): string; + encodeFunctionData(functionFragment: "token", values?: undefined): string; + encodeFunctionData( + functionFragment: "totalSupply", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "unpauseStakingRewards", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "unstake", + values: [PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "unstakeEscrow", + values: [PromiseOrValue, PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "userRewardPerTokenPaid", + values: [PromiseOrValue] + ): string; + + decodeFunctionResult( + functionFragment: "_totalSupply", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "acceptOwnership", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "balanceOf", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "earned", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "escrowedBalanceOf", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "exit", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "getReward", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "getRewardForDuration", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "lastTimeRewardApplicable", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "lastUpdateTime", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "nominateNewOwner", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "nominatedOwner", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "nonEscrowedBalanceOf", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "notifyRewardAmount", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "owner", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "pauseStakingRewards", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "paused", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "periodFinish", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "recoverERC20", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "rewardEscrow", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "rewardPerToken", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "rewardPerTokenStored", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "rewardRate", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "rewards", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "rewardsDuration", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "setRewardsDuration", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "stake", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "stakeEscrow", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "supplySchedule", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "token", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "totalSupply", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "unpauseStakingRewards", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "unstake", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "unstakeEscrow", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "userRewardPerTokenPaid", + data: BytesLike + ): Result; + + events: { + "EscrowStaked(address,uint256)": EventFragment; + "EscrowUnstaked(address,uint256)": EventFragment; + "OwnerChanged(address,address)": EventFragment; + "OwnerNominated(address)": EventFragment; + "Paused(address)": EventFragment; + "Recovered(address,uint256)": EventFragment; + "RewardAdded(uint256)": EventFragment; + "RewardPaid(address,uint256)": EventFragment; + "RewardsDurationUpdated(uint256)": EventFragment; + "Staked(address,uint256)": EventFragment; + "Unpaused(address)": EventFragment; + "Unstaked(address,uint256)": EventFragment; + }; + + getEvent(nameOrSignatureOrTopic: "EscrowStaked"): EventFragment; + getEvent(nameOrSignatureOrTopic: "EscrowUnstaked"): EventFragment; + getEvent(nameOrSignatureOrTopic: "OwnerChanged"): EventFragment; + getEvent(nameOrSignatureOrTopic: "OwnerNominated"): EventFragment; + getEvent(nameOrSignatureOrTopic: "Paused"): EventFragment; + getEvent(nameOrSignatureOrTopic: "Recovered"): EventFragment; + getEvent(nameOrSignatureOrTopic: "RewardAdded"): EventFragment; + getEvent(nameOrSignatureOrTopic: "RewardPaid"): EventFragment; + getEvent(nameOrSignatureOrTopic: "RewardsDurationUpdated"): EventFragment; + getEvent(nameOrSignatureOrTopic: "Staked"): EventFragment; + getEvent(nameOrSignatureOrTopic: "Unpaused"): EventFragment; + getEvent(nameOrSignatureOrTopic: "Unstaked"): EventFragment; +} + +export interface EscrowStakedEventObject { + user: string; + amount: BigNumber; +} +export type EscrowStakedEvent = TypedEvent< + [string, BigNumber], + EscrowStakedEventObject +>; + +export type EscrowStakedEventFilter = TypedEventFilter; + +export interface EscrowUnstakedEventObject { + user: string; + amount: BigNumber; +} +export type EscrowUnstakedEvent = TypedEvent< + [string, BigNumber], + EscrowUnstakedEventObject +>; + +export type EscrowUnstakedEventFilter = TypedEventFilter; + +export interface OwnerChangedEventObject { + oldOwner: string; + newOwner: string; +} +export type OwnerChangedEvent = TypedEvent< + [string, string], + OwnerChangedEventObject +>; + +export type OwnerChangedEventFilter = TypedEventFilter; + +export interface OwnerNominatedEventObject { + newOwner: string; +} +export type OwnerNominatedEvent = TypedEvent< + [string], + OwnerNominatedEventObject +>; + +export type OwnerNominatedEventFilter = TypedEventFilter; + +export interface PausedEventObject { + account: string; +} +export type PausedEvent = TypedEvent<[string], PausedEventObject>; + +export type PausedEventFilter = TypedEventFilter; + +export interface RecoveredEventObject { + token: string; + amount: BigNumber; +} +export type RecoveredEvent = TypedEvent< + [string, BigNumber], + RecoveredEventObject +>; + +export type RecoveredEventFilter = TypedEventFilter; + +export interface RewardAddedEventObject { + reward: BigNumber; +} +export type RewardAddedEvent = TypedEvent<[BigNumber], RewardAddedEventObject>; + +export type RewardAddedEventFilter = TypedEventFilter; + +export interface RewardPaidEventObject { + user: string; + reward: BigNumber; +} +export type RewardPaidEvent = TypedEvent< + [string, BigNumber], + RewardPaidEventObject +>; + +export type RewardPaidEventFilter = TypedEventFilter; + +export interface RewardsDurationUpdatedEventObject { + newDuration: BigNumber; +} +export type RewardsDurationUpdatedEvent = TypedEvent< + [BigNumber], + RewardsDurationUpdatedEventObject +>; + +export type RewardsDurationUpdatedEventFilter = + TypedEventFilter; + +export interface StakedEventObject { + user: string; + amount: BigNumber; +} +export type StakedEvent = TypedEvent<[string, BigNumber], StakedEventObject>; + +export type StakedEventFilter = TypedEventFilter; + +export interface UnpausedEventObject { + account: string; +} +export type UnpausedEvent = TypedEvent<[string], UnpausedEventObject>; + +export type UnpausedEventFilter = TypedEventFilter; + +export interface UnstakedEventObject { + user: string; + amount: BigNumber; +} +export type UnstakedEvent = TypedEvent< + [string, BigNumber], + UnstakedEventObject +>; + +export type UnstakedEventFilter = TypedEventFilter; + +export interface KwentaStakingRewards extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + interface: KwentaStakingRewardsInterface; + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>; + + listeners( + eventFilter?: TypedEventFilter + ): Array>; + listeners(eventName?: string): Array; + removeAllListeners( + eventFilter: TypedEventFilter + ): this; + removeAllListeners(eventName?: string): this; + off: OnEvent; + on: OnEvent; + once: OnEvent; + removeListener: OnEvent; + + functions: { + _totalSupply(overrides?: CallOverrides): Promise<[BigNumber]>; + + acceptOwnership( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + balanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise<[BigNumber]>; + + earned( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise<[BigNumber]>; + + escrowedBalanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise<[BigNumber]>; + + exit( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + getReward( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + getRewardForDuration(overrides?: CallOverrides): Promise<[BigNumber]>; + + lastTimeRewardApplicable(overrides?: CallOverrides): Promise<[BigNumber]>; + + lastUpdateTime(overrides?: CallOverrides): Promise<[BigNumber]>; + + nominateNewOwner( + _owner: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + nominatedOwner(overrides?: CallOverrides): Promise<[string]>; + + nonEscrowedBalanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise<[BigNumber]>; + + notifyRewardAmount( + reward: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + owner(overrides?: CallOverrides): Promise<[string]>; + + pauseStakingRewards( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + paused(overrides?: CallOverrides): Promise<[boolean]>; + + periodFinish(overrides?: CallOverrides): Promise<[BigNumber]>; + + recoverERC20( + tokenAddress: PromiseOrValue, + tokenAmount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + rewardEscrow(overrides?: CallOverrides): Promise<[string]>; + + rewardPerToken(overrides?: CallOverrides): Promise<[BigNumber]>; + + rewardPerTokenStored(overrides?: CallOverrides): Promise<[BigNumber]>; + + rewardRate(overrides?: CallOverrides): Promise<[BigNumber]>; + + rewards( + arg0: PromiseOrValue, + overrides?: CallOverrides + ): Promise<[BigNumber]>; + + rewardsDuration(overrides?: CallOverrides): Promise<[BigNumber]>; + + setRewardsDuration( + _rewardsDuration: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + stake( + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + stakeEscrow( + account: PromiseOrValue, + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + supplySchedule(overrides?: CallOverrides): Promise<[string]>; + + token(overrides?: CallOverrides): Promise<[string]>; + + totalSupply(overrides?: CallOverrides): Promise<[BigNumber]>; + + unpauseStakingRewards( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + unstake( + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + unstakeEscrow( + account: PromiseOrValue, + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + userRewardPerTokenPaid( + arg0: PromiseOrValue, + overrides?: CallOverrides + ): Promise<[BigNumber]>; + }; + + _totalSupply(overrides?: CallOverrides): Promise; + + acceptOwnership( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + balanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + earned( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + escrowedBalanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + exit( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + getReward( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + getRewardForDuration(overrides?: CallOverrides): Promise; + + lastTimeRewardApplicable(overrides?: CallOverrides): Promise; + + lastUpdateTime(overrides?: CallOverrides): Promise; + + nominateNewOwner( + _owner: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + nominatedOwner(overrides?: CallOverrides): Promise; + + nonEscrowedBalanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + notifyRewardAmount( + reward: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + owner(overrides?: CallOverrides): Promise; + + pauseStakingRewards( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + paused(overrides?: CallOverrides): Promise; + + periodFinish(overrides?: CallOverrides): Promise; + + recoverERC20( + tokenAddress: PromiseOrValue, + tokenAmount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + rewardEscrow(overrides?: CallOverrides): Promise; + + rewardPerToken(overrides?: CallOverrides): Promise; + + rewardPerTokenStored(overrides?: CallOverrides): Promise; + + rewardRate(overrides?: CallOverrides): Promise; + + rewards( + arg0: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + rewardsDuration(overrides?: CallOverrides): Promise; + + setRewardsDuration( + _rewardsDuration: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + stake( + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + stakeEscrow( + account: PromiseOrValue, + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + supplySchedule(overrides?: CallOverrides): Promise; + + token(overrides?: CallOverrides): Promise; + + totalSupply(overrides?: CallOverrides): Promise; + + unpauseStakingRewards( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + unstake( + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + unstakeEscrow( + account: PromiseOrValue, + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + userRewardPerTokenPaid( + arg0: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + callStatic: { + _totalSupply(overrides?: CallOverrides): Promise; + + acceptOwnership(overrides?: CallOverrides): Promise; + + balanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + earned( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + escrowedBalanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + exit(overrides?: CallOverrides): Promise; + + getReward(overrides?: CallOverrides): Promise; + + getRewardForDuration(overrides?: CallOverrides): Promise; + + lastTimeRewardApplicable(overrides?: CallOverrides): Promise; + + lastUpdateTime(overrides?: CallOverrides): Promise; + + nominateNewOwner( + _owner: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + nominatedOwner(overrides?: CallOverrides): Promise; + + nonEscrowedBalanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + notifyRewardAmount( + reward: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + owner(overrides?: CallOverrides): Promise; + + pauseStakingRewards(overrides?: CallOverrides): Promise; + + paused(overrides?: CallOverrides): Promise; + + periodFinish(overrides?: CallOverrides): Promise; + + recoverERC20( + tokenAddress: PromiseOrValue, + tokenAmount: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + rewardEscrow(overrides?: CallOverrides): Promise; + + rewardPerToken(overrides?: CallOverrides): Promise; + + rewardPerTokenStored(overrides?: CallOverrides): Promise; + + rewardRate(overrides?: CallOverrides): Promise; + + rewards( + arg0: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + rewardsDuration(overrides?: CallOverrides): Promise; + + setRewardsDuration( + _rewardsDuration: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + stake( + amount: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + stakeEscrow( + account: PromiseOrValue, + amount: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + supplySchedule(overrides?: CallOverrides): Promise; + + token(overrides?: CallOverrides): Promise; + + totalSupply(overrides?: CallOverrides): Promise; + + unpauseStakingRewards(overrides?: CallOverrides): Promise; + + unstake( + amount: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + unstakeEscrow( + account: PromiseOrValue, + amount: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + userRewardPerTokenPaid( + arg0: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + }; + + filters: { + "EscrowStaked(address,uint256)"( + user?: PromiseOrValue | null, + amount?: null + ): EscrowStakedEventFilter; + EscrowStaked( + user?: PromiseOrValue | null, + amount?: null + ): EscrowStakedEventFilter; + + "EscrowUnstaked(address,uint256)"( + user?: null, + amount?: null + ): EscrowUnstakedEventFilter; + EscrowUnstaked(user?: null, amount?: null): EscrowUnstakedEventFilter; + + "OwnerChanged(address,address)"( + oldOwner?: null, + newOwner?: null + ): OwnerChangedEventFilter; + OwnerChanged(oldOwner?: null, newOwner?: null): OwnerChangedEventFilter; + + "OwnerNominated(address)"(newOwner?: null): OwnerNominatedEventFilter; + OwnerNominated(newOwner?: null): OwnerNominatedEventFilter; + + "Paused(address)"(account?: null): PausedEventFilter; + Paused(account?: null): PausedEventFilter; + + "Recovered(address,uint256)"( + token?: null, + amount?: null + ): RecoveredEventFilter; + Recovered(token?: null, amount?: null): RecoveredEventFilter; + + "RewardAdded(uint256)"(reward?: null): RewardAddedEventFilter; + RewardAdded(reward?: null): RewardAddedEventFilter; + + "RewardPaid(address,uint256)"( + user?: PromiseOrValue | null, + reward?: null + ): RewardPaidEventFilter; + RewardPaid( + user?: PromiseOrValue | null, + reward?: null + ): RewardPaidEventFilter; + + "RewardsDurationUpdated(uint256)"( + newDuration?: null + ): RewardsDurationUpdatedEventFilter; + RewardsDurationUpdated( + newDuration?: null + ): RewardsDurationUpdatedEventFilter; + + "Staked(address,uint256)"( + user?: PromiseOrValue | null, + amount?: null + ): StakedEventFilter; + Staked( + user?: PromiseOrValue | null, + amount?: null + ): StakedEventFilter; + + "Unpaused(address)"(account?: null): UnpausedEventFilter; + Unpaused(account?: null): UnpausedEventFilter; + + "Unstaked(address,uint256)"( + user?: PromiseOrValue | null, + amount?: null + ): UnstakedEventFilter; + Unstaked( + user?: PromiseOrValue | null, + amount?: null + ): UnstakedEventFilter; + }; + + estimateGas: { + _totalSupply(overrides?: CallOverrides): Promise; + + acceptOwnership( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + balanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + earned( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + escrowedBalanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + exit( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + getReward( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + getRewardForDuration(overrides?: CallOverrides): Promise; + + lastTimeRewardApplicable(overrides?: CallOverrides): Promise; + + lastUpdateTime(overrides?: CallOverrides): Promise; + + nominateNewOwner( + _owner: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + nominatedOwner(overrides?: CallOverrides): Promise; + + nonEscrowedBalanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + notifyRewardAmount( + reward: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + owner(overrides?: CallOverrides): Promise; + + pauseStakingRewards( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + paused(overrides?: CallOverrides): Promise; + + periodFinish(overrides?: CallOverrides): Promise; + + recoverERC20( + tokenAddress: PromiseOrValue, + tokenAmount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + rewardEscrow(overrides?: CallOverrides): Promise; + + rewardPerToken(overrides?: CallOverrides): Promise; + + rewardPerTokenStored(overrides?: CallOverrides): Promise; + + rewardRate(overrides?: CallOverrides): Promise; + + rewards( + arg0: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + rewardsDuration(overrides?: CallOverrides): Promise; + + setRewardsDuration( + _rewardsDuration: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + stake( + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + stakeEscrow( + account: PromiseOrValue, + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + supplySchedule(overrides?: CallOverrides): Promise; + + token(overrides?: CallOverrides): Promise; + + totalSupply(overrides?: CallOverrides): Promise; + + unpauseStakingRewards( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + unstake( + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + unstakeEscrow( + account: PromiseOrValue, + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + userRewardPerTokenPaid( + arg0: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + }; + + populateTransaction: { + _totalSupply(overrides?: CallOverrides): Promise; + + acceptOwnership( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + balanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + earned( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + escrowedBalanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + exit( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + getReward( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + getRewardForDuration( + overrides?: CallOverrides + ): Promise; + + lastTimeRewardApplicable( + overrides?: CallOverrides + ): Promise; + + lastUpdateTime(overrides?: CallOverrides): Promise; + + nominateNewOwner( + _owner: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + nominatedOwner(overrides?: CallOverrides): Promise; + + nonEscrowedBalanceOf( + account: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + notifyRewardAmount( + reward: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + owner(overrides?: CallOverrides): Promise; + + pauseStakingRewards( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + paused(overrides?: CallOverrides): Promise; + + periodFinish(overrides?: CallOverrides): Promise; + + recoverERC20( + tokenAddress: PromiseOrValue, + tokenAmount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + rewardEscrow(overrides?: CallOverrides): Promise; + + rewardPerToken(overrides?: CallOverrides): Promise; + + rewardPerTokenStored( + overrides?: CallOverrides + ): Promise; + + rewardRate(overrides?: CallOverrides): Promise; + + rewards( + arg0: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + + rewardsDuration(overrides?: CallOverrides): Promise; + + setRewardsDuration( + _rewardsDuration: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + stake( + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + stakeEscrow( + account: PromiseOrValue, + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + supplySchedule(overrides?: CallOverrides): Promise; + + token(overrides?: CallOverrides): Promise; + + totalSupply(overrides?: CallOverrides): Promise; + + unpauseStakingRewards( + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + unstake( + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + unstakeEscrow( + account: PromiseOrValue, + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + + userRewardPerTokenPaid( + arg0: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + }; +} diff --git a/sdk/contracts/types/StakingRewards.ts b/sdk/contracts/types/StakingRewards.ts index 6c65ea9333..0483e9bf17 100644 --- a/sdk/contracts/types/StakingRewards.ts +++ b/sdk/contracts/types/StakingRewards.ts @@ -29,86 +29,74 @@ import type { export interface StakingRewardsInterface extends utils.Interface { functions: { - "_totalSupply()": FunctionFragment; "acceptOwnership()": FunctionFragment; "balanceOf(address)": FunctionFragment; "earned(address)": FunctionFragment; - "escrowedBalanceOf(address)": FunctionFragment; "exit()": FunctionFragment; "getReward()": FunctionFragment; "getRewardForDuration()": FunctionFragment; + "lastPauseTime()": FunctionFragment; "lastTimeRewardApplicable()": FunctionFragment; "lastUpdateTime()": FunctionFragment; "nominateNewOwner(address)": FunctionFragment; "nominatedOwner()": FunctionFragment; - "nonEscrowedBalanceOf(address)": FunctionFragment; "notifyRewardAmount(uint256)": FunctionFragment; "owner()": FunctionFragment; - "pauseStakingRewards()": FunctionFragment; "paused()": FunctionFragment; "periodFinish()": FunctionFragment; "recoverERC20(address,uint256)": FunctionFragment; - "rewardEscrow()": FunctionFragment; "rewardPerToken()": FunctionFragment; "rewardPerTokenStored()": FunctionFragment; "rewardRate()": FunctionFragment; "rewards(address)": FunctionFragment; + "rewardsDistribution()": FunctionFragment; "rewardsDuration()": FunctionFragment; + "rewardsToken()": FunctionFragment; + "setPaused(bool)": FunctionFragment; + "setRewardsDistribution(address)": FunctionFragment; "setRewardsDuration(uint256)": FunctionFragment; "stake(uint256)": FunctionFragment; - "stakeEscrow(address,uint256)": FunctionFragment; - "supplySchedule()": FunctionFragment; - "token()": FunctionFragment; + "stakingToken()": FunctionFragment; "totalSupply()": FunctionFragment; - "unpauseStakingRewards()": FunctionFragment; - "unstake(uint256)": FunctionFragment; - "unstakeEscrow(address,uint256)": FunctionFragment; "userRewardPerTokenPaid(address)": FunctionFragment; + "withdraw(uint256)": FunctionFragment; }; getFunction( nameOrSignatureOrTopic: - | "_totalSupply" | "acceptOwnership" | "balanceOf" | "earned" - | "escrowedBalanceOf" | "exit" | "getReward" | "getRewardForDuration" + | "lastPauseTime" | "lastTimeRewardApplicable" | "lastUpdateTime" | "nominateNewOwner" | "nominatedOwner" - | "nonEscrowedBalanceOf" | "notifyRewardAmount" | "owner" - | "pauseStakingRewards" | "paused" | "periodFinish" | "recoverERC20" - | "rewardEscrow" | "rewardPerToken" | "rewardPerTokenStored" | "rewardRate" | "rewards" + | "rewardsDistribution" | "rewardsDuration" + | "rewardsToken" + | "setPaused" + | "setRewardsDistribution" | "setRewardsDuration" | "stake" - | "stakeEscrow" - | "supplySchedule" - | "token" + | "stakingToken" | "totalSupply" - | "unpauseStakingRewards" - | "unstake" - | "unstakeEscrow" | "userRewardPerTokenPaid" + | "withdraw" ): FunctionFragment; - encodeFunctionData( - functionFragment: "_totalSupply", - values?: undefined - ): string; encodeFunctionData( functionFragment: "acceptOwnership", values?: undefined @@ -121,16 +109,16 @@ export interface StakingRewardsInterface extends utils.Interface { functionFragment: "earned", values: [PromiseOrValue] ): string; - encodeFunctionData( - functionFragment: "escrowedBalanceOf", - values: [PromiseOrValue] - ): string; encodeFunctionData(functionFragment: "exit", values?: undefined): string; encodeFunctionData(functionFragment: "getReward", values?: undefined): string; encodeFunctionData( functionFragment: "getRewardForDuration", values?: undefined ): string; + encodeFunctionData( + functionFragment: "lastPauseTime", + values?: undefined + ): string; encodeFunctionData( functionFragment: "lastTimeRewardApplicable", values?: undefined @@ -147,19 +135,11 @@ export interface StakingRewardsInterface extends utils.Interface { functionFragment: "nominatedOwner", values?: undefined ): string; - encodeFunctionData( - functionFragment: "nonEscrowedBalanceOf", - values: [PromiseOrValue] - ): string; encodeFunctionData( functionFragment: "notifyRewardAmount", values: [PromiseOrValue] ): string; encodeFunctionData(functionFragment: "owner", values?: undefined): string; - encodeFunctionData( - functionFragment: "pauseStakingRewards", - values?: undefined - ): string; encodeFunctionData(functionFragment: "paused", values?: undefined): string; encodeFunctionData( functionFragment: "periodFinish", @@ -169,10 +149,6 @@ export interface StakingRewardsInterface extends utils.Interface { functionFragment: "recoverERC20", values: [PromiseOrValue, PromiseOrValue] ): string; - encodeFunctionData( - functionFragment: "rewardEscrow", - values?: undefined - ): string; encodeFunctionData( functionFragment: "rewardPerToken", values?: undefined @@ -189,10 +165,26 @@ export interface StakingRewardsInterface extends utils.Interface { functionFragment: "rewards", values: [PromiseOrValue] ): string; + encodeFunctionData( + functionFragment: "rewardsDistribution", + values?: undefined + ): string; encodeFunctionData( functionFragment: "rewardsDuration", values?: undefined ): string; + encodeFunctionData( + functionFragment: "rewardsToken", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "setPaused", + values: [PromiseOrValue] + ): string; + encodeFunctionData( + functionFragment: "setRewardsDistribution", + values: [PromiseOrValue] + ): string; encodeFunctionData( functionFragment: "setRewardsDuration", values: [PromiseOrValue] @@ -202,55 +194,38 @@ export interface StakingRewardsInterface extends utils.Interface { values: [PromiseOrValue] ): string; encodeFunctionData( - functionFragment: "stakeEscrow", - values: [PromiseOrValue, PromiseOrValue] - ): string; - encodeFunctionData( - functionFragment: "supplySchedule", + functionFragment: "stakingToken", values?: undefined ): string; - encodeFunctionData(functionFragment: "token", values?: undefined): string; encodeFunctionData( functionFragment: "totalSupply", values?: undefined ): string; encodeFunctionData( - functionFragment: "unpauseStakingRewards", - values?: undefined + functionFragment: "userRewardPerTokenPaid", + values: [PromiseOrValue] ): string; encodeFunctionData( - functionFragment: "unstake", + functionFragment: "withdraw", values: [PromiseOrValue] ): string; - encodeFunctionData( - functionFragment: "unstakeEscrow", - values: [PromiseOrValue, PromiseOrValue] - ): string; - encodeFunctionData( - functionFragment: "userRewardPerTokenPaid", - values: [PromiseOrValue] - ): string; - decodeFunctionResult( - functionFragment: "_totalSupply", - data: BytesLike - ): Result; decodeFunctionResult( functionFragment: "acceptOwnership", data: BytesLike ): Result; decodeFunctionResult(functionFragment: "balanceOf", data: BytesLike): Result; decodeFunctionResult(functionFragment: "earned", data: BytesLike): Result; - decodeFunctionResult( - functionFragment: "escrowedBalanceOf", - data: BytesLike - ): Result; decodeFunctionResult(functionFragment: "exit", data: BytesLike): Result; decodeFunctionResult(functionFragment: "getReward", data: BytesLike): Result; decodeFunctionResult( functionFragment: "getRewardForDuration", data: BytesLike ): Result; + decodeFunctionResult( + functionFragment: "lastPauseTime", + data: BytesLike + ): Result; decodeFunctionResult( functionFragment: "lastTimeRewardApplicable", data: BytesLike @@ -267,19 +242,11 @@ export interface StakingRewardsInterface extends utils.Interface { functionFragment: "nominatedOwner", data: BytesLike ): Result; - decodeFunctionResult( - functionFragment: "nonEscrowedBalanceOf", - data: BytesLike - ): Result; decodeFunctionResult( functionFragment: "notifyRewardAmount", data: BytesLike ): Result; decodeFunctionResult(functionFragment: "owner", data: BytesLike): Result; - decodeFunctionResult( - functionFragment: "pauseStakingRewards", - data: BytesLike - ): Result; decodeFunctionResult(functionFragment: "paused", data: BytesLike): Result; decodeFunctionResult( functionFragment: "periodFinish", @@ -289,10 +256,6 @@ export interface StakingRewardsInterface extends utils.Interface { functionFragment: "recoverERC20", data: BytesLike ): Result; - decodeFunctionResult( - functionFragment: "rewardEscrow", - data: BytesLike - ): Result; decodeFunctionResult( functionFragment: "rewardPerToken", data: BytesLike @@ -304,92 +267,64 @@ export interface StakingRewardsInterface extends utils.Interface { decodeFunctionResult(functionFragment: "rewardRate", data: BytesLike): Result; decodeFunctionResult(functionFragment: "rewards", data: BytesLike): Result; decodeFunctionResult( - functionFragment: "rewardsDuration", + functionFragment: "rewardsDistribution", data: BytesLike ): Result; decodeFunctionResult( - functionFragment: "setRewardsDuration", + functionFragment: "rewardsDuration", data: BytesLike ): Result; - decodeFunctionResult(functionFragment: "stake", data: BytesLike): Result; decodeFunctionResult( - functionFragment: "stakeEscrow", + functionFragment: "rewardsToken", data: BytesLike ): Result; + decodeFunctionResult(functionFragment: "setPaused", data: BytesLike): Result; decodeFunctionResult( - functionFragment: "supplySchedule", + functionFragment: "setRewardsDistribution", data: BytesLike ): Result; - decodeFunctionResult(functionFragment: "token", data: BytesLike): Result; decodeFunctionResult( - functionFragment: "totalSupply", + functionFragment: "setRewardsDuration", data: BytesLike ): Result; + decodeFunctionResult(functionFragment: "stake", data: BytesLike): Result; decodeFunctionResult( - functionFragment: "unpauseStakingRewards", + functionFragment: "stakingToken", data: BytesLike ): Result; - decodeFunctionResult(functionFragment: "unstake", data: BytesLike): Result; decodeFunctionResult( - functionFragment: "unstakeEscrow", + functionFragment: "totalSupply", data: BytesLike ): Result; decodeFunctionResult( functionFragment: "userRewardPerTokenPaid", data: BytesLike ): Result; + decodeFunctionResult(functionFragment: "withdraw", data: BytesLike): Result; events: { - "EscrowStaked(address,uint256)": EventFragment; - "EscrowUnstaked(address,uint256)": EventFragment; "OwnerChanged(address,address)": EventFragment; "OwnerNominated(address)": EventFragment; - "Paused(address)": EventFragment; + "PauseChanged(bool)": EventFragment; "Recovered(address,uint256)": EventFragment; "RewardAdded(uint256)": EventFragment; "RewardPaid(address,uint256)": EventFragment; "RewardsDurationUpdated(uint256)": EventFragment; "Staked(address,uint256)": EventFragment; - "Unpaused(address)": EventFragment; - "Unstaked(address,uint256)": EventFragment; + "Withdrawn(address,uint256)": EventFragment; }; - getEvent(nameOrSignatureOrTopic: "EscrowStaked"): EventFragment; - getEvent(nameOrSignatureOrTopic: "EscrowUnstaked"): EventFragment; getEvent(nameOrSignatureOrTopic: "OwnerChanged"): EventFragment; getEvent(nameOrSignatureOrTopic: "OwnerNominated"): EventFragment; - getEvent(nameOrSignatureOrTopic: "Paused"): EventFragment; + getEvent(nameOrSignatureOrTopic: "PauseChanged"): EventFragment; getEvent(nameOrSignatureOrTopic: "Recovered"): EventFragment; getEvent(nameOrSignatureOrTopic: "RewardAdded"): EventFragment; getEvent(nameOrSignatureOrTopic: "RewardPaid"): EventFragment; getEvent(nameOrSignatureOrTopic: "RewardsDurationUpdated"): EventFragment; getEvent(nameOrSignatureOrTopic: "Staked"): EventFragment; - getEvent(nameOrSignatureOrTopic: "Unpaused"): EventFragment; - getEvent(nameOrSignatureOrTopic: "Unstaked"): EventFragment; + getEvent(nameOrSignatureOrTopic: "Withdrawn"): EventFragment; } -export interface EscrowStakedEventObject { - user: string; - amount: BigNumber; -} -export type EscrowStakedEvent = TypedEvent< - [string, BigNumber], - EscrowStakedEventObject ->; - -export type EscrowStakedEventFilter = TypedEventFilter; - -export interface EscrowUnstakedEventObject { - user: string; - amount: BigNumber; -} -export type EscrowUnstakedEvent = TypedEvent< - [string, BigNumber], - EscrowUnstakedEventObject ->; - -export type EscrowUnstakedEventFilter = TypedEventFilter; - export interface OwnerChangedEventObject { oldOwner: string; newOwner: string; @@ -411,12 +346,12 @@ export type OwnerNominatedEvent = TypedEvent< export type OwnerNominatedEventFilter = TypedEventFilter; -export interface PausedEventObject { - account: string; +export interface PauseChangedEventObject { + isPaused: boolean; } -export type PausedEvent = TypedEvent<[string], PausedEventObject>; +export type PauseChangedEvent = TypedEvent<[boolean], PauseChangedEventObject>; -export type PausedEventFilter = TypedEventFilter; +export type PauseChangedEventFilter = TypedEventFilter; export interface RecoveredEventObject { token: string; @@ -466,23 +401,16 @@ export type StakedEvent = TypedEvent<[string, BigNumber], StakedEventObject>; export type StakedEventFilter = TypedEventFilter; -export interface UnpausedEventObject { - account: string; -} -export type UnpausedEvent = TypedEvent<[string], UnpausedEventObject>; - -export type UnpausedEventFilter = TypedEventFilter; - -export interface UnstakedEventObject { +export interface WithdrawnEventObject { user: string; amount: BigNumber; } -export type UnstakedEvent = TypedEvent< +export type WithdrawnEvent = TypedEvent< [string, BigNumber], - UnstakedEventObject + WithdrawnEventObject >; -export type UnstakedEventFilter = TypedEventFilter; +export type WithdrawnEventFilter = TypedEventFilter; export interface StakingRewards extends BaseContract { connect(signerOrProvider: Signer | Provider | string): this; @@ -511,8 +439,6 @@ export interface StakingRewards extends BaseContract { removeListener: OnEvent; functions: { - _totalSupply(overrides?: CallOverrides): Promise<[BigNumber]>; - acceptOwnership( overrides?: Overrides & { from?: PromiseOrValue } ): Promise; @@ -527,11 +453,6 @@ export interface StakingRewards extends BaseContract { overrides?: CallOverrides ): Promise<[BigNumber]>; - escrowedBalanceOf( - account: PromiseOrValue, - overrides?: CallOverrides - ): Promise<[BigNumber]>; - exit( overrides?: Overrides & { from?: PromiseOrValue } ): Promise; @@ -542,6 +463,8 @@ export interface StakingRewards extends BaseContract { getRewardForDuration(overrides?: CallOverrides): Promise<[BigNumber]>; + lastPauseTime(overrides?: CallOverrides): Promise<[BigNumber]>; + lastTimeRewardApplicable(overrides?: CallOverrides): Promise<[BigNumber]>; lastUpdateTime(overrides?: CallOverrides): Promise<[BigNumber]>; @@ -553,11 +476,6 @@ export interface StakingRewards extends BaseContract { nominatedOwner(overrides?: CallOverrides): Promise<[string]>; - nonEscrowedBalanceOf( - account: PromiseOrValue, - overrides?: CallOverrides - ): Promise<[BigNumber]>; - notifyRewardAmount( reward: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } @@ -565,10 +483,6 @@ export interface StakingRewards extends BaseContract { owner(overrides?: CallOverrides): Promise<[string]>; - pauseStakingRewards( - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; - paused(overrides?: CallOverrides): Promise<[boolean]>; periodFinish(overrides?: CallOverrides): Promise<[BigNumber]>; @@ -579,8 +493,6 @@ export interface StakingRewards extends BaseContract { overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - rewardEscrow(overrides?: CallOverrides): Promise<[string]>; - rewardPerToken(overrides?: CallOverrides): Promise<[BigNumber]>; rewardPerTokenStored(overrides?: CallOverrides): Promise<[BigNumber]>; @@ -592,52 +504,46 @@ export interface StakingRewards extends BaseContract { overrides?: CallOverrides ): Promise<[BigNumber]>; + rewardsDistribution(overrides?: CallOverrides): Promise<[string]>; + rewardsDuration(overrides?: CallOverrides): Promise<[BigNumber]>; - setRewardsDuration( - _rewardsDuration: PromiseOrValue, - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; + rewardsToken(overrides?: CallOverrides): Promise<[string]>; - stake( - amount: PromiseOrValue, + setPaused( + _paused: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - stakeEscrow( - account: PromiseOrValue, - amount: PromiseOrValue, + setRewardsDistribution( + _rewardsDistribution: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - supplySchedule(overrides?: CallOverrides): Promise<[string]>; - - token(overrides?: CallOverrides): Promise<[string]>; - - totalSupply(overrides?: CallOverrides): Promise<[BigNumber]>; - - unpauseStakingRewards( + setRewardsDuration( + _rewardsDuration: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - unstake( + stake( amount: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - unstakeEscrow( - account: PromiseOrValue, - amount: PromiseOrValue, - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; + stakingToken(overrides?: CallOverrides): Promise<[string]>; + + totalSupply(overrides?: CallOverrides): Promise<[BigNumber]>; userRewardPerTokenPaid( arg0: PromiseOrValue, overrides?: CallOverrides ): Promise<[BigNumber]>; - }; - _totalSupply(overrides?: CallOverrides): Promise; + withdraw( + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + }; acceptOwnership( overrides?: Overrides & { from?: PromiseOrValue } @@ -653,11 +559,6 @@ export interface StakingRewards extends BaseContract { overrides?: CallOverrides ): Promise; - escrowedBalanceOf( - account: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - exit( overrides?: Overrides & { from?: PromiseOrValue } ): Promise; @@ -668,6 +569,8 @@ export interface StakingRewards extends BaseContract { getRewardForDuration(overrides?: CallOverrides): Promise; + lastPauseTime(overrides?: CallOverrides): Promise; + lastTimeRewardApplicable(overrides?: CallOverrides): Promise; lastUpdateTime(overrides?: CallOverrides): Promise; @@ -679,11 +582,6 @@ export interface StakingRewards extends BaseContract { nominatedOwner(overrides?: CallOverrides): Promise; - nonEscrowedBalanceOf( - account: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - notifyRewardAmount( reward: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } @@ -691,10 +589,6 @@ export interface StakingRewards extends BaseContract { owner(overrides?: CallOverrides): Promise; - pauseStakingRewards( - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; - paused(overrides?: CallOverrides): Promise; periodFinish(overrides?: CallOverrides): Promise; @@ -705,8 +599,6 @@ export interface StakingRewards extends BaseContract { overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - rewardEscrow(overrides?: CallOverrides): Promise; - rewardPerToken(overrides?: CallOverrides): Promise; rewardPerTokenStored(overrides?: CallOverrides): Promise; @@ -718,53 +610,47 @@ export interface StakingRewards extends BaseContract { overrides?: CallOverrides ): Promise; + rewardsDistribution(overrides?: CallOverrides): Promise; + rewardsDuration(overrides?: CallOverrides): Promise; - setRewardsDuration( - _rewardsDuration: PromiseOrValue, - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; + rewardsToken(overrides?: CallOverrides): Promise; - stake( - amount: PromiseOrValue, + setPaused( + _paused: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - stakeEscrow( - account: PromiseOrValue, - amount: PromiseOrValue, + setRewardsDistribution( + _rewardsDistribution: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - supplySchedule(overrides?: CallOverrides): Promise; - - token(overrides?: CallOverrides): Promise; - - totalSupply(overrides?: CallOverrides): Promise; - - unpauseStakingRewards( + setRewardsDuration( + _rewardsDuration: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - unstake( + stake( amount: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - unstakeEscrow( - account: PromiseOrValue, - amount: PromiseOrValue, - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; + stakingToken(overrides?: CallOverrides): Promise; + + totalSupply(overrides?: CallOverrides): Promise; userRewardPerTokenPaid( arg0: PromiseOrValue, overrides?: CallOverrides ): Promise; - callStatic: { - _totalSupply(overrides?: CallOverrides): Promise; + withdraw( + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + callStatic: { acceptOwnership(overrides?: CallOverrides): Promise; balanceOf( @@ -777,17 +663,14 @@ export interface StakingRewards extends BaseContract { overrides?: CallOverrides ): Promise; - escrowedBalanceOf( - account: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - exit(overrides?: CallOverrides): Promise; getReward(overrides?: CallOverrides): Promise; getRewardForDuration(overrides?: CallOverrides): Promise; + lastPauseTime(overrides?: CallOverrides): Promise; + lastTimeRewardApplicable(overrides?: CallOverrides): Promise; lastUpdateTime(overrides?: CallOverrides): Promise; @@ -799,11 +682,6 @@ export interface StakingRewards extends BaseContract { nominatedOwner(overrides?: CallOverrides): Promise; - nonEscrowedBalanceOf( - account: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - notifyRewardAmount( reward: PromiseOrValue, overrides?: CallOverrides @@ -811,8 +689,6 @@ export interface StakingRewards extends BaseContract { owner(overrides?: CallOverrides): Promise; - pauseStakingRewards(overrides?: CallOverrides): Promise; - paused(overrides?: CallOverrides): Promise; periodFinish(overrides?: CallOverrides): Promise; @@ -823,8 +699,6 @@ export interface StakingRewards extends BaseContract { overrides?: CallOverrides ): Promise; - rewardEscrow(overrides?: CallOverrides): Promise; - rewardPerToken(overrides?: CallOverrides): Promise; rewardPerTokenStored(overrides?: CallOverrides): Promise; @@ -836,65 +710,48 @@ export interface StakingRewards extends BaseContract { overrides?: CallOverrides ): Promise; + rewardsDistribution(overrides?: CallOverrides): Promise; + rewardsDuration(overrides?: CallOverrides): Promise; - setRewardsDuration( - _rewardsDuration: PromiseOrValue, - overrides?: CallOverrides - ): Promise; + rewardsToken(overrides?: CallOverrides): Promise; - stake( - amount: PromiseOrValue, + setPaused( + _paused: PromiseOrValue, overrides?: CallOverrides ): Promise; - stakeEscrow( - account: PromiseOrValue, - amount: PromiseOrValue, + setRewardsDistribution( + _rewardsDistribution: PromiseOrValue, overrides?: CallOverrides ): Promise; - supplySchedule(overrides?: CallOverrides): Promise; - - token(overrides?: CallOverrides): Promise; - - totalSupply(overrides?: CallOverrides): Promise; - - unpauseStakingRewards(overrides?: CallOverrides): Promise; - - unstake( - amount: PromiseOrValue, + setRewardsDuration( + _rewardsDuration: PromiseOrValue, overrides?: CallOverrides ): Promise; - unstakeEscrow( - account: PromiseOrValue, + stake( amount: PromiseOrValue, overrides?: CallOverrides ): Promise; + stakingToken(overrides?: CallOverrides): Promise; + + totalSupply(overrides?: CallOverrides): Promise; + userRewardPerTokenPaid( arg0: PromiseOrValue, overrides?: CallOverrides ): Promise; + + withdraw( + amount: PromiseOrValue, + overrides?: CallOverrides + ): Promise; }; filters: { - "EscrowStaked(address,uint256)"( - user?: PromiseOrValue | null, - amount?: null - ): EscrowStakedEventFilter; - EscrowStaked( - user?: PromiseOrValue | null, - amount?: null - ): EscrowStakedEventFilter; - - "EscrowUnstaked(address,uint256)"( - user?: null, - amount?: null - ): EscrowUnstakedEventFilter; - EscrowUnstaked(user?: null, amount?: null): EscrowUnstakedEventFilter; - "OwnerChanged(address,address)"( oldOwner?: null, newOwner?: null @@ -904,8 +761,8 @@ export interface StakingRewards extends BaseContract { "OwnerNominated(address)"(newOwner?: null): OwnerNominatedEventFilter; OwnerNominated(newOwner?: null): OwnerNominatedEventFilter; - "Paused(address)"(account?: null): PausedEventFilter; - Paused(account?: null): PausedEventFilter; + "PauseChanged(bool)"(isPaused?: null): PauseChangedEventFilter; + PauseChanged(isPaused?: null): PauseChangedEventFilter; "Recovered(address,uint256)"( token?: null, @@ -941,22 +798,17 @@ export interface StakingRewards extends BaseContract { amount?: null ): StakedEventFilter; - "Unpaused(address)"(account?: null): UnpausedEventFilter; - Unpaused(account?: null): UnpausedEventFilter; - - "Unstaked(address,uint256)"( + "Withdrawn(address,uint256)"( user?: PromiseOrValue | null, amount?: null - ): UnstakedEventFilter; - Unstaked( + ): WithdrawnEventFilter; + Withdrawn( user?: PromiseOrValue | null, amount?: null - ): UnstakedEventFilter; + ): WithdrawnEventFilter; }; estimateGas: { - _totalSupply(overrides?: CallOverrides): Promise; - acceptOwnership( overrides?: Overrides & { from?: PromiseOrValue } ): Promise; @@ -971,11 +823,6 @@ export interface StakingRewards extends BaseContract { overrides?: CallOverrides ): Promise; - escrowedBalanceOf( - account: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - exit( overrides?: Overrides & { from?: PromiseOrValue } ): Promise; @@ -986,6 +833,8 @@ export interface StakingRewards extends BaseContract { getRewardForDuration(overrides?: CallOverrides): Promise; + lastPauseTime(overrides?: CallOverrides): Promise; + lastTimeRewardApplicable(overrides?: CallOverrides): Promise; lastUpdateTime(overrides?: CallOverrides): Promise; @@ -997,11 +846,6 @@ export interface StakingRewards extends BaseContract { nominatedOwner(overrides?: CallOverrides): Promise; - nonEscrowedBalanceOf( - account: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - notifyRewardAmount( reward: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } @@ -1009,10 +853,6 @@ export interface StakingRewards extends BaseContract { owner(overrides?: CallOverrides): Promise; - pauseStakingRewards( - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; - paused(overrides?: CallOverrides): Promise; periodFinish(overrides?: CallOverrides): Promise; @@ -1023,8 +863,6 @@ export interface StakingRewards extends BaseContract { overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - rewardEscrow(overrides?: CallOverrides): Promise; - rewardPerToken(overrides?: CallOverrides): Promise; rewardPerTokenStored(overrides?: CallOverrides): Promise; @@ -1036,54 +874,48 @@ export interface StakingRewards extends BaseContract { overrides?: CallOverrides ): Promise; + rewardsDistribution(overrides?: CallOverrides): Promise; + rewardsDuration(overrides?: CallOverrides): Promise; - setRewardsDuration( - _rewardsDuration: PromiseOrValue, - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; + rewardsToken(overrides?: CallOverrides): Promise; - stake( - amount: PromiseOrValue, + setPaused( + _paused: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - stakeEscrow( - account: PromiseOrValue, - amount: PromiseOrValue, + setRewardsDistribution( + _rewardsDistribution: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - supplySchedule(overrides?: CallOverrides): Promise; - - token(overrides?: CallOverrides): Promise; - - totalSupply(overrides?: CallOverrides): Promise; - - unpauseStakingRewards( + setRewardsDuration( + _rewardsDuration: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - unstake( + stake( amount: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - unstakeEscrow( - account: PromiseOrValue, - amount: PromiseOrValue, - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; + stakingToken(overrides?: CallOverrides): Promise; + + totalSupply(overrides?: CallOverrides): Promise; userRewardPerTokenPaid( arg0: PromiseOrValue, overrides?: CallOverrides ): Promise; + + withdraw( + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; }; populateTransaction: { - _totalSupply(overrides?: CallOverrides): Promise; - acceptOwnership( overrides?: Overrides & { from?: PromiseOrValue } ): Promise; @@ -1098,11 +930,6 @@ export interface StakingRewards extends BaseContract { overrides?: CallOverrides ): Promise; - escrowedBalanceOf( - account: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - exit( overrides?: Overrides & { from?: PromiseOrValue } ): Promise; @@ -1115,6 +942,8 @@ export interface StakingRewards extends BaseContract { overrides?: CallOverrides ): Promise; + lastPauseTime(overrides?: CallOverrides): Promise; + lastTimeRewardApplicable( overrides?: CallOverrides ): Promise; @@ -1128,11 +957,6 @@ export interface StakingRewards extends BaseContract { nominatedOwner(overrides?: CallOverrides): Promise; - nonEscrowedBalanceOf( - account: PromiseOrValue, - overrides?: CallOverrides - ): Promise; - notifyRewardAmount( reward: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } @@ -1140,10 +964,6 @@ export interface StakingRewards extends BaseContract { owner(overrides?: CallOverrides): Promise; - pauseStakingRewards( - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; - paused(overrides?: CallOverrides): Promise; periodFinish(overrides?: CallOverrides): Promise; @@ -1154,8 +974,6 @@ export interface StakingRewards extends BaseContract { overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - rewardEscrow(overrides?: CallOverrides): Promise; - rewardPerToken(overrides?: CallOverrides): Promise; rewardPerTokenStored( @@ -1169,48 +987,46 @@ export interface StakingRewards extends BaseContract { overrides?: CallOverrides ): Promise; + rewardsDistribution( + overrides?: CallOverrides + ): Promise; + rewardsDuration(overrides?: CallOverrides): Promise; - setRewardsDuration( - _rewardsDuration: PromiseOrValue, - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; + rewardsToken(overrides?: CallOverrides): Promise; - stake( - amount: PromiseOrValue, + setPaused( + _paused: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - stakeEscrow( - account: PromiseOrValue, - amount: PromiseOrValue, + setRewardsDistribution( + _rewardsDistribution: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - supplySchedule(overrides?: CallOverrides): Promise; - - token(overrides?: CallOverrides): Promise; - - totalSupply(overrides?: CallOverrides): Promise; - - unpauseStakingRewards( + setRewardsDuration( + _rewardsDuration: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - unstake( + stake( amount: PromiseOrValue, overrides?: Overrides & { from?: PromiseOrValue } ): Promise; - unstakeEscrow( - account: PromiseOrValue, - amount: PromiseOrValue, - overrides?: Overrides & { from?: PromiseOrValue } - ): Promise; + stakingToken(overrides?: CallOverrides): Promise; + + totalSupply(overrides?: CallOverrides): Promise; userRewardPerTokenPaid( arg0: PromiseOrValue, overrides?: CallOverrides ): Promise; + + withdraw( + amount: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; }; } diff --git a/sdk/contracts/types/factories/KwentaArrakisVault__factory.ts b/sdk/contracts/types/factories/KwentaArrakisVault__factory.ts new file mode 100644 index 0000000000..cac72f4fa6 --- /dev/null +++ b/sdk/contracts/types/factories/KwentaArrakisVault__factory.ts @@ -0,0 +1,169 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Signer, utils } from "ethers"; +import type { Provider } from "@ethersproject/providers"; +import type { + KwentaArrakisVault, + KwentaArrakisVaultInterface, +} from "../KwentaArrakisVault"; + +const _abi = [ + { + inputs: [ + { + internalType: "address", + name: "implementationAddress", + type: "address", + }, + { + internalType: "address", + name: "adminAddress", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + stateMutability: "payable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousAdmin", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newAdmin", + type: "address", + }, + ], + name: "ProxyAdminTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousImplementation", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newImplementation", + type: "address", + }, + ], + name: "ProxyImplementationUpdated", + type: "event", + }, + { + stateMutability: "payable", + type: "fallback", + }, + { + inputs: [], + name: "proxyAdmin", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "id", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newAdmin", + type: "address", + }, + ], + name: "transferProxyAdmin", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newImplementation", + type: "address", + }, + ], + name: "upgradeTo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newImplementation", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "upgradeToAndCall", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, +]; + +export class KwentaArrakisVault__factory { + static readonly abi = _abi; + static createInterface(): KwentaArrakisVaultInterface { + return new utils.Interface(_abi) as KwentaArrakisVaultInterface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider + ): KwentaArrakisVault { + return new Contract(address, _abi, signerOrProvider) as KwentaArrakisVault; + } +} diff --git a/sdk/contracts/types/factories/KwentaStakingRewards__factory.ts b/sdk/contracts/types/factories/KwentaStakingRewards__factory.ts new file mode 100644 index 0000000000..bd2d0658af --- /dev/null +++ b/sdk/contracts/types/factories/KwentaStakingRewards__factory.ts @@ -0,0 +1,725 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Signer, utils } from "ethers"; +import type { Provider } from "@ethersproject/providers"; +import type { + KwentaStakingRewards, + KwentaStakingRewardsInterface, +} from "../KwentaStakingRewards"; + +const _abi = [ + { + inputs: [ + { + internalType: "address", + name: "_token", + type: "address", + }, + { + internalType: "address", + name: "_rewardEscrow", + type: "address", + }, + { + internalType: "address", + name: "_supplySchedule", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "user", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "EscrowStaked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "user", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "EscrowUnstaked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "oldOwner", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnerChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnerNominated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "Paused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Recovered", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "reward", + type: "uint256", + }, + ], + name: "RewardAdded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "user", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "reward", + type: "uint256", + }, + ], + name: "RewardPaid", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "newDuration", + type: "uint256", + }, + ], + name: "RewardsDurationUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "user", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Staked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "Unpaused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "user", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Unstaked", + type: "event", + }, + { + inputs: [], + name: "_totalSupply", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "acceptOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "balanceOf", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "earned", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "escrowedBalanceOf", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "exit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "getReward", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "getRewardForDuration", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "lastTimeRewardApplicable", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "lastUpdateTime", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_owner", + type: "address", + }, + ], + name: "nominateNewOwner", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "nominatedOwner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "nonEscrowedBalanceOf", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "reward", + type: "uint256", + }, + ], + name: "notifyRewardAmount", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pauseStakingRewards", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "paused", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "periodFinish", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "tokenAddress", + type: "address", + }, + { + internalType: "uint256", + name: "tokenAmount", + type: "uint256", + }, + ], + name: "recoverERC20", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "rewardEscrow", + outputs: [ + { + internalType: "contract IRewardEscrow", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "rewardPerToken", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "rewardPerTokenStored", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "rewardRate", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "rewards", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "rewardsDuration", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_rewardsDuration", + type: "uint256", + }, + ], + name: "setRewardsDuration", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "stake", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "stakeEscrow", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "supplySchedule", + outputs: [ + { + internalType: "contract ISupplySchedule", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "token", + outputs: [ + { + internalType: "contract IERC20", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalSupply", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "unpauseStakingRewards", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "unstake", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "unstakeEscrow", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "userRewardPerTokenPaid", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, +]; + +export class KwentaStakingRewards__factory { + static readonly abi = _abi; + static createInterface(): KwentaStakingRewardsInterface { + return new utils.Interface(_abi) as KwentaStakingRewardsInterface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider + ): KwentaStakingRewards { + return new Contract( + address, + _abi, + signerOrProvider + ) as KwentaStakingRewards; + } +} diff --git a/sdk/contracts/types/factories/StakingRewards__factory.ts b/sdk/contracts/types/factories/StakingRewards__factory.ts index 23e649102e..65087957dd 100644 --- a/sdk/contracts/types/factories/StakingRewards__factory.ts +++ b/sdk/contracts/types/factories/StakingRewards__factory.ts @@ -14,60 +14,28 @@ const _abi = [ inputs: [ { internalType: "address", - name: "_token", + name: "_owner", type: "address", }, { internalType: "address", - name: "_rewardEscrow", + name: "_rewardsDistribution", type: "address", }, { internalType: "address", - name: "_supplySchedule", + name: "_rewardsToken", type: "address", }, - ], - stateMutability: "nonpayable", - type: "constructor", - }, - { - anonymous: false, - inputs: [ { - indexed: true, internalType: "address", - name: "user", + name: "_stakingToken", type: "address", }, - { - indexed: false, - internalType: "uint256", - name: "amount", - type: "uint256", - }, ], - name: "EscrowStaked", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "address", - name: "user", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "amount", - type: "uint256", - }, - ], - name: "EscrowUnstaked", - type: "event", + payable: false, + stateMutability: "nonpayable", + type: "constructor", }, { anonymous: false, @@ -106,12 +74,12 @@ const _abi = [ inputs: [ { indexed: false, - internalType: "address", - name: "account", - type: "address", + internalType: "bool", + name: "isPaused", + type: "bool", }, ], - name: "Paused", + name: "PauseChanged", type: "event", }, { @@ -197,19 +165,6 @@ const _abi = [ name: "Staked", type: "event", }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "Unpaused", - type: "event", - }, { anonymous: false, inputs: [ @@ -226,30 +181,20 @@ const _abi = [ type: "uint256", }, ], - name: "Unstaked", + name: "Withdrawn", type: "event", }, { - inputs: [], - name: "_totalSupply", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { + constant: false, inputs: [], name: "acceptOwnership", outputs: [], + payable: false, stateMutability: "nonpayable", type: "function", }, { + constant: true, inputs: [ { internalType: "address", @@ -265,10 +210,12 @@ const _abi = [ type: "uint256", }, ], + payable: false, stateMutability: "view", type: "function", }, { + constant: true, inputs: [ { internalType: "address", @@ -284,43 +231,30 @@ const _abi = [ type: "uint256", }, ], + payable: false, stateMutability: "view", type: "function", }, { - inputs: [ - { - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "escrowedBalanceOf", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { + constant: false, inputs: [], name: "exit", outputs: [], + payable: false, stateMutability: "nonpayable", type: "function", }, { + constant: false, inputs: [], name: "getReward", outputs: [], + payable: false, stateMutability: "nonpayable", type: "function", }, { + constant: true, inputs: [], name: "getRewardForDuration", outputs: [ @@ -330,10 +264,27 @@ const _abi = [ type: "uint256", }, ], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: true, + inputs: [], + name: "lastPauseTime", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + payable: false, stateMutability: "view", type: "function", }, { + constant: true, inputs: [], name: "lastTimeRewardApplicable", outputs: [ @@ -343,10 +294,12 @@ const _abi = [ type: "uint256", }, ], + payable: false, stateMutability: "view", type: "function", }, { + constant: true, inputs: [], name: "lastUpdateTime", outputs: [ @@ -356,10 +309,12 @@ const _abi = [ type: "uint256", }, ], + payable: false, stateMutability: "view", type: "function", }, { + constant: false, inputs: [ { internalType: "address", @@ -369,10 +324,12 @@ const _abi = [ ], name: "nominateNewOwner", outputs: [], + payable: false, stateMutability: "nonpayable", type: "function", }, { + constant: true, inputs: [], name: "nominatedOwner", outputs: [ @@ -382,29 +339,12 @@ const _abi = [ type: "address", }, ], + payable: false, stateMutability: "view", type: "function", }, { - inputs: [ - { - internalType: "address", - name: "account", - type: "address", - }, - ], - name: "nonEscrowedBalanceOf", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, - { + constant: false, inputs: [ { internalType: "uint256", @@ -414,10 +354,12 @@ const _abi = [ ], name: "notifyRewardAmount", outputs: [], + payable: false, stateMutability: "nonpayable", type: "function", }, { + constant: true, inputs: [], name: "owner", outputs: [ @@ -427,17 +369,12 @@ const _abi = [ type: "address", }, ], + payable: false, stateMutability: "view", type: "function", }, { - inputs: [], - name: "pauseStakingRewards", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { + constant: true, inputs: [], name: "paused", outputs: [ @@ -447,10 +384,12 @@ const _abi = [ type: "bool", }, ], + payable: false, stateMutability: "view", type: "function", }, { + constant: true, inputs: [], name: "periodFinish", outputs: [ @@ -460,10 +399,12 @@ const _abi = [ type: "uint256", }, ], + payable: false, stateMutability: "view", type: "function", }, { + constant: false, inputs: [ { internalType: "address", @@ -478,23 +419,12 @@ const _abi = [ ], name: "recoverERC20", outputs: [], + payable: false, stateMutability: "nonpayable", type: "function", }, { - inputs: [], - name: "rewardEscrow", - outputs: [ - { - internalType: "contract IRewardEscrow", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { + constant: true, inputs: [], name: "rewardPerToken", outputs: [ @@ -504,10 +434,12 @@ const _abi = [ type: "uint256", }, ], + payable: false, stateMutability: "view", type: "function", }, { + constant: true, inputs: [], name: "rewardPerTokenStored", outputs: [ @@ -517,10 +449,12 @@ const _abi = [ type: "uint256", }, ], + payable: false, stateMutability: "view", type: "function", }, { + constant: true, inputs: [], name: "rewardRate", outputs: [ @@ -530,10 +464,12 @@ const _abi = [ type: "uint256", }, ], + payable: false, stateMutability: "view", type: "function", }, { + constant: true, inputs: [ { internalType: "address", @@ -549,10 +485,27 @@ const _abi = [ type: "uint256", }, ], + payable: false, stateMutability: "view", type: "function", }, { + constant: true, + inputs: [], + name: "rewardsDistribution", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: true, inputs: [], name: "rewardsDuration", outputs: [ @@ -562,69 +515,89 @@ const _abi = [ type: "uint256", }, ], + payable: false, stateMutability: "view", type: "function", }, { - inputs: [ + constant: true, + inputs: [], + name: "rewardsToken", + outputs: [ { - internalType: "uint256", - name: "_rewardsDuration", - type: "uint256", + internalType: "contract IERC20", + name: "", + type: "address", }, ], - name: "setRewardsDuration", - outputs: [], - stateMutability: "nonpayable", + payable: false, + stateMutability: "view", type: "function", }, { + constant: false, inputs: [ { - internalType: "uint256", - name: "amount", - type: "uint256", + internalType: "bool", + name: "_paused", + type: "bool", }, ], - name: "stake", + name: "setPaused", outputs: [], + payable: false, stateMutability: "nonpayable", type: "function", }, { + constant: false, inputs: [ { internalType: "address", - name: "account", + name: "_rewardsDistribution", type: "address", }, + ], + name: "setRewardsDistribution", + outputs: [], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: false, + inputs: [ { internalType: "uint256", - name: "amount", + name: "_rewardsDuration", type: "uint256", }, ], - name: "stakeEscrow", + name: "setRewardsDuration", outputs: [], + payable: false, stateMutability: "nonpayable", type: "function", }, { - inputs: [], - name: "supplySchedule", - outputs: [ + constant: false, + inputs: [ { - internalType: "contract ISupplySchedule", - name: "", - type: "address", + internalType: "uint256", + name: "amount", + type: "uint256", }, ], - stateMutability: "view", + name: "stake", + outputs: [], + payable: false, + stateMutability: "nonpayable", type: "function", }, { + constant: true, inputs: [], - name: "token", + name: "stakingToken", outputs: [ { internalType: "contract IERC20", @@ -632,10 +605,12 @@ const _abi = [ type: "address", }, ], + payable: false, stateMutability: "view", type: "function", }, { + constant: true, inputs: [], name: "totalSupply", outputs: [ @@ -645,64 +620,44 @@ const _abi = [ type: "uint256", }, ], + payable: false, stateMutability: "view", type: "function", }, { - inputs: [], - name: "unpauseStakingRewards", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "amount", - type: "uint256", - }, - ], - name: "unstake", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { + constant: true, inputs: [ { internalType: "address", - name: "account", + name: "", type: "address", }, + ], + name: "userRewardPerTokenPaid", + outputs: [ { internalType: "uint256", - name: "amount", + name: "", type: "uint256", }, ], - name: "unstakeEscrow", - outputs: [], - stateMutability: "nonpayable", + payable: false, + stateMutability: "view", type: "function", }, { + constant: false, inputs: [ - { - internalType: "address", - name: "", - type: "address", - }, - ], - name: "userRewardPerTokenPaid", - outputs: [ { internalType: "uint256", - name: "", + name: "amount", type: "uint256", }, ], - stateMutability: "view", + name: "withdraw", + outputs: [], + payable: false, + stateMutability: "nonpayable", type: "function", }, ]; diff --git a/sdk/contracts/types/factories/index.ts b/sdk/contracts/types/factories/index.ts index e125e14a28..a78c18abdb 100644 --- a/sdk/contracts/types/factories/index.ts +++ b/sdk/contracts/types/factories/index.ts @@ -10,6 +10,8 @@ export { Exchanger__factory } from "./Exchanger__factory"; export { FuturesMarket__factory } from "./FuturesMarket__factory"; export { FuturesMarketData__factory } from "./FuturesMarketData__factory"; export { FuturesMarketSettings__factory } from "./FuturesMarketSettings__factory"; +export { KwentaArrakisVault__factory } from "./KwentaArrakisVault__factory"; +export { KwentaStakingRewards__factory } from "./KwentaStakingRewards__factory"; export { MultipleMerkleDistributor__factory } from "./MultipleMerkleDistributor__factory"; export { ReverseRecords__factory } from "./ReverseRecords__factory"; export { RewardEscrow__factory } from "./RewardEscrow__factory"; diff --git a/sdk/contracts/types/index.ts b/sdk/contracts/types/index.ts index 4805b2ba20..5e27d557b7 100644 --- a/sdk/contracts/types/index.ts +++ b/sdk/contracts/types/index.ts @@ -10,6 +10,8 @@ export type { Exchanger } from "./Exchanger"; export type { FuturesMarket } from "./FuturesMarket"; export type { FuturesMarketData } from "./FuturesMarketData"; export type { FuturesMarketSettings } from "./FuturesMarketSettings"; +export type { KwentaArrakisVault } from "./KwentaArrakisVault"; +export type { KwentaStakingRewards } from "./KwentaStakingRewards"; export type { MultipleMerkleDistributor } from "./MultipleMerkleDistributor"; export type { ReverseRecords } from "./ReverseRecords"; export type { RewardEscrow } from "./RewardEscrow"; @@ -34,6 +36,8 @@ export { ExchangeRates__factory } from "./factories/ExchangeRates__factory"; export { FuturesMarket__factory } from "./factories/FuturesMarket__factory"; export { FuturesMarketData__factory } from "./factories/FuturesMarketData__factory"; export { FuturesMarketSettings__factory } from "./factories/FuturesMarketSettings__factory"; +export { KwentaArrakisVault__factory } from "./factories/KwentaArrakisVault__factory"; +export { KwentaStakingRewards__factory } from "./factories/KwentaStakingRewards__factory"; export { MultipleMerkleDistributor__factory } from "./factories/MultipleMerkleDistributor__factory"; export { ReverseRecords__factory } from "./factories/ReverseRecords__factory"; export { RewardEscrow__factory } from "./factories/RewardEscrow__factory"; diff --git a/sdk/index.ts b/sdk/index.ts index ff93a4af65..ed3a72a520 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -5,8 +5,8 @@ import { ethers } from 'ethers'; import Context, { IContext } from './context'; import ExchangeService from './services/exchange'; import FuturesService from './services/futures'; +import KwentaTokenService from './services/kwentaToken'; import SynthsService from './services/synths'; -import TokenService from './services/token'; import TransactionsService from './services/transactions'; export default class KwentaSDK { @@ -17,7 +17,7 @@ export default class KwentaSDK { public futures: FuturesService; public synths: SynthsService; public transactions: TransactionsService; - public token: TokenService; + public kwentaToken: KwentaTokenService; constructor(context: IContext) { this.context = new Context(context); @@ -25,7 +25,7 @@ export default class KwentaSDK { this.futures = new FuturesService(this); this.synths = new SynthsService(this); this.transactions = new TransactionsService(this); - this.token = new TokenService(this); + this.kwentaToken = new KwentaTokenService(this); } public setProvider(provider: ethers.providers.Provider) { diff --git a/sdk/services/kwentaToken.ts b/sdk/services/kwentaToken.ts new file mode 100644 index 0000000000..4844f2d4bd --- /dev/null +++ b/sdk/services/kwentaToken.ts @@ -0,0 +1,439 @@ +import Wei, { wei } from '@synthetixio/wei'; +import axios from 'axios'; +import { ethers, BigNumber } from 'ethers'; +import moment from 'moment'; +import KwentaSDK from 'sdk'; + +import { DEFAULT_NUMBER_OF_FUTURES_FEE } from 'constants/defaults'; +import { FLEEK_BASE_URL, FLEEK_STORAGE_BUCKET } from 'queries/files/constants'; +import { ContractName } from 'sdk/contracts'; +import { formatTruncatedDuration } from 'utils/formatters/date'; + +import * as sdkErrors from '../common/errors'; + +const client = axios.create({ + baseURL: `${FLEEK_BASE_URL}/${FLEEK_STORAGE_BUCKET}/data/`, + timeout: 5000, +}); + +export type ClaimParams = [number, string, string, string[], number]; + +type EpochData = { + merkleRoot: string; + tokenTotal: string; + claims: { + [address: string]: { + index: number; + amount: string; + proof: string[]; + }; + }; +}; + +export type EscrowData = { + id: number; + date: string; + time: string; + vestable: T; + amount: T; + fee: T; + status: 'VESTING' | 'VESTED'; +}; + +export default class KwentaTokenService { + private sdk: KwentaSDK; + + constructor(sdk: KwentaSDK) { + this.sdk = sdk; + } + + public changePoolTokens(amount: string, action: 'stake' | 'withdraw') { + if (!this.sdk.context.contracts.StakingRewards) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + return this.sdk.transactions.createContractTxn( + this.sdk.context.contracts.StakingRewards, + action, + [wei(amount).toBN()] + ); + } + + public approveLPToken() { + return this.approveToken('KwentaArrakisVault', 'StakingRewards'); + } + + public async getEarnDetails() { + const { StakingRewards, KwentaArrakisVault } = this.sdk.context.mutliCallContracts; + + if (!StakingRewards || !KwentaArrakisVault) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + const { walletAddress } = this.sdk.context; + + const [ + balance, + earned, + periodFinish, + rewardRate, + totalSupply, + lpTokenBalance, + allowance, + ]: BigNumber[] = await this.sdk.context.multicallProvider.all([ + StakingRewards.balanceOf(walletAddress), + StakingRewards.earned(walletAddress), + StakingRewards.periodFinish(), + StakingRewards.rewardRate(), + StakingRewards.totalSupply(), + KwentaArrakisVault.balanceOf(walletAddress), + KwentaArrakisVault.allowance(walletAddress, StakingRewards.address), + ]); + + return { + balance: wei(balance), + earned: wei(earned), + endDate: periodFinish.toNumber(), + rewardRate: wei(rewardRate), + totalSupply: wei(totalSupply), + lpTokenBalance: wei(lpTokenBalance), + allowance: wei(allowance), + }; + } + + public claimRewards() { + const StakingRewards = this.sdk.context.contracts.StakingRewards; + + if (!StakingRewards) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + return this.sdk.transactions.createContractTxn(StakingRewards, 'getReward', []); + } + + public async getStakingData() { + const { vKwentaRedeemer, veKwentaRedeemer } = this.sdk.context.contracts; + + const { + RewardEscrow, + KwentaStakingRewards, + KwentaToken, + SupplySchedule, + vKwentaToken, + veKwentaToken, + MultipleMerkleDistributor, + } = this.sdk.context.mutliCallContracts; + + if ( + !RewardEscrow || + !KwentaStakingRewards || + !KwentaToken || + !SupplySchedule || + !vKwentaToken || + !MultipleMerkleDistributor || + !veKwentaToken || + !vKwentaRedeemer || + !veKwentaRedeemer + ) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + const { walletAddress } = this.sdk.context; + + const [ + rewardEscrowBalance, + stakedNonEscrowedBalance, + stakedEscrowedBalance, + claimableBalance, + kwentaBalance, + weekCounter, + totalStakedBalance, + vKwentaBalance, + vKwentaAllowance, + kwentaAllowance, + epochPeriod, + veKwentaBalance, + veKwentaAllowance, + ]: BigNumber[] = await this.sdk.context.multicallProvider.all([ + RewardEscrow.balanceOf(walletAddress), + KwentaStakingRewards.nonEscrowedBalanceOf(walletAddress), + KwentaStakingRewards.escrowedBalanceOf(walletAddress), + KwentaStakingRewards.earned(walletAddress), + KwentaToken.balanceOf(walletAddress), + SupplySchedule.weekCounter(), + KwentaStakingRewards.totalSupply(), + vKwentaToken.balanceOf(walletAddress), + vKwentaToken.allowance(walletAddress, vKwentaRedeemer.address), + KwentaToken.allowance(walletAddress, KwentaStakingRewards.address), + MultipleMerkleDistributor.distributionEpoch(), + veKwentaToken.balanceOf(walletAddress), + veKwentaToken.allowance(walletAddress, veKwentaRedeemer.address), + ]); + + return { + rewardEscrowBalance: wei(rewardEscrowBalance), + stakedNonEscrowedBalance: wei(stakedNonEscrowedBalance), + stakedEscrowedBalance: wei(stakedEscrowedBalance), + claimableBalance: wei(claimableBalance), + kwentaBalance: wei(kwentaBalance), + weekCounter: Number(weekCounter), + totalStakedBalance: wei(totalStakedBalance), + vKwentaBalance: wei(vKwentaBalance), + vKwentaAllowance: wei(vKwentaAllowance), + kwentaAllowance: wei(kwentaAllowance), + epochPeriod: Number(epochPeriod), + veKwentaBalance: wei(veKwentaBalance), + veKwentaAllowance: wei(veKwentaAllowance), + }; + } + + public async getEscrowData() { + const { RewardEscrow } = this.sdk.context.contracts; + const { RewardEscrow: RewardEscrowMulticall } = this.sdk.context.mutliCallContracts; + + if (!RewardEscrow || !RewardEscrowMulticall) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + const { walletAddress } = this.sdk.context; + + const schedules = await RewardEscrow.getVestingSchedules( + walletAddress, + 0, + DEFAULT_NUMBER_OF_FUTURES_FEE + ); + + const vestingSchedules = schedules.filter((schedule) => schedule.escrowAmount.gt(0)); + + const calls = vestingSchedules.map((schedule) => + RewardEscrowMulticall.getVestingEntryClaimable(walletAddress, schedule.entryID) + ); + + const vestingEntries: { + quantity: BigNumber; + fee: BigNumber; + }[] = await this.sdk.context.multicallProvider.all(calls); + + const { escrowData, totalVestable } = vestingSchedules.reduce( + (acc, next, i) => { + const vestable = wei(vestingEntries[i].quantity); + const date = Number(next.endTime) * 1000; + + acc.totalVestable = acc.totalVestable.add(vestable); + + acc.escrowData.push({ + id: Number(next.entryID), + date: moment(date).format('MM/DD/YY'), + time: formatTruncatedDuration(Number(next.endTime) - new Date().getTime() / 1000), + vestable, + amount: wei(next.escrowAmount), + fee: wei(vestingEntries[i].fee), + status: date > Date.now() ? 'VESTING' : 'VESTED', + }); + + return acc; + }, + { escrowData: [] as EscrowData[], totalVestable: wei(0) } + ); + + return { escrowData, totalVestable }; + } + + public getReward() { + const { KwentaStakingRewards } = this.sdk.context.contracts; + + if (!KwentaStakingRewards) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + return this.sdk.transactions.createContractTxn(KwentaStakingRewards, 'getReward', []); + } + + // TODO: Replace this with separate functions that use `approveToken` + // In that case, we can safely remove the map object from this method. + + public approveKwentaToken( + token: 'kwenta' | 'vKwenta' | 'veKwenta', + amount = ethers.constants.MaxUint256 + ) { + const { + KwentaToken, + KwentaStakingRewards, + vKwentaToken, + vKwentaRedeemer, + veKwentaToken, + veKwentaRedeemer, + } = this.sdk.context.contracts; + + const map = { + kwenta: { contract: KwentaToken, spender: KwentaStakingRewards }, + vKwenta: { contract: vKwentaToken, spender: vKwentaRedeemer }, + veKwenta: { contract: veKwentaToken, spender: veKwentaRedeemer }, + }; + + const { contract, spender } = map[token]; + + if (!contract || !spender) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + return this.sdk.transactions.createContractTxn(contract, 'approve', [spender.address, amount]); + } + + public approveToken( + token: ContractName, + spender?: ContractName, + amount = ethers.constants.MaxUint256 + ) { + const tokenContract = this.sdk.context.contracts[token]; + + if (!tokenContract) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + let spenderAddress = this.sdk.context.walletAddress; + + if (spender) { + const spenderContract = this.sdk.context.contracts[spender]; + if (spenderContract) spenderAddress = spenderContract.address; + } + + return this.sdk.transactions.createContractTxn(tokenContract, 'approve', [ + spenderAddress, + amount, + ]); + } + + public redeemToken( + token: ContractName, + options: { hasAddress: boolean } = { hasAddress: false } + ) { + const tokenContract = this.sdk.context.contracts[token]; + + if (!tokenContract) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + return this.sdk.transactions.createContractTxn( + tokenContract, + 'redeem', + options.hasAddress ? [this.sdk.context.walletAddress] : [] + ); + } + + public redeemVKwenta() { + return this.redeemToken('vKwentaRedeemer'); + } + + public redeemVeKwenta() { + return this.redeemToken('veKwentaRedeemer', { hasAddress: true }); + } + + public vestToken(ids: number[]) { + const { RewardEscrow } = this.sdk.context.contracts; + + if (!RewardEscrow) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + return this.sdk.transactions.createContractTxn(RewardEscrow, 'vest', [ids]); + } + + public stakeKwenta(amount: string | BigNumber) { + return this.performStakeAction('stake', amount); + } + + public unstakeKwenta(amount: string | BigNumber) { + return this.performStakeAction('unstake', amount); + } + + public stakeEscrowedKwenta(amount: string | BigNumber) { + return this.performStakeAction('stake', amount, { escrow: true }); + } + + public unstakeEscrowedKwenta(amount: string | BigNumber) { + return this.performStakeAction('unstake', amount, { escrow: true }); + } + + public async getClaimableRewards(periods: number[]) { + const { MultipleMerkleDistributor } = this.sdk.context.mutliCallContracts; + const { walletAddress } = this.sdk.context; + + if (!MultipleMerkleDistributor) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + const fileNames = periods + .slice(0, -1) + .map( + (i) => + `trading-rewards-snapshots/${ + this.sdk.context.networkId === 420 ? `goerli-` : '' + }epoch-${i}.json` + ); + + const responses: EpochData[] = []; + + for (const fileName of fileNames) { + const response = await client.get(fileName); + responses.push(response.data ?? null); + } + + const rewards = responses + .map((d, period) => { + const walletReward = d.claims[walletAddress]; + return [walletReward.index, walletAddress, walletReward.amount, walletReward.proof, period]; + }) + .filter((x): x is ClaimParams => !!x); + + const claimed: boolean[] = await this.sdk.context.multicallProvider.all( + rewards.map((reward) => MultipleMerkleDistributor.isClaimed(reward[0], reward[4])) + ); + + const { totalRewards, claimableRewards } = rewards.reduce( + (acc, next, i) => { + if (!claimed[i]) { + acc.claimableRewards.push(next); + acc.totalRewards = acc.totalRewards.add(wei(next[2])); + } + + return acc; + }, + { claimableRewards: [] as ClaimParams[], totalRewards: wei(0) } + ); + + return { claimableRewards, totalRewards }; + } + + public async claimMultipleRewards(claimableRewards: ClaimParams[]) { + const { MultipleMerkleDistributor } = this.sdk.context.contracts; + + if (!MultipleMerkleDistributor) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + return this.sdk.transactions.createContractTxn(MultipleMerkleDistributor, 'claimMultiple', [ + claimableRewards, + ]); + } + + private performStakeAction( + action: 'stake' | 'unstake', + amount: string | BigNumber, + options: { escrow: boolean } = { escrow: false } + ) { + const { RewardEscrow, KwentaStakingRewards } = this.sdk.context.contracts; + + if (!RewardEscrow || !KwentaStakingRewards) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + const contract = options?.escrow ? RewardEscrow : KwentaStakingRewards; + + return this.sdk.transactions.createContractTxn( + contract, + `${action}${options?.escrow ? 'Escrow' : ''}`, + [amount] + ); + } +} diff --git a/sdk/services/token.ts b/sdk/services/token.ts deleted file mode 100644 index 8fb6338414..0000000000 --- a/sdk/services/token.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { wei } from '@synthetixio/wei'; -import { ethers } from 'ethers'; -import KwentaSDK from 'sdk'; - -import * as sdkErrors from '../common/errors'; - -export default class TokenService { - private sdk: KwentaSDK; - - constructor(sdk: KwentaSDK) { - this.sdk = sdk; - } - - public async changePoolTokens(amount: string, action: 'stake' | 'withdraw') { - if (!this.sdk.context.contracts.StakingRewards) { - throw new Error(sdkErrors.UNSUPPORTED_NETWORK); - } - - const { hash } = await this.sdk.transactions.createContractTxn( - this.sdk.context.contracts.StakingRewards, - action, - [wei(amount).toBN()] - ); - - return hash; - } - - public async approveLPToken() { - const StakingRewards = this.sdk.context.contracts.StakingRewards; - const KwentaArrakisVault = this.sdk.context.contracts.KwentaArrakisVault; - - if (!StakingRewards || !KwentaArrakisVault) { - throw new Error(sdkErrors.UNSUPPORTED_NETWORK); - } - - const { hash } = await this.sdk.transactions.createContractTxn(KwentaArrakisVault, 'approve', [ - StakingRewards.address, - ethers.constants.MaxUint256, - ]); - - return hash; - } - - public async getEarnDetails() { - const StakingRewards = this.sdk.context.contracts.StakingRewards; - const KwentaArrakisVault = this.sdk.context.contracts.KwentaArrakisVault; - - if (!StakingRewards || !KwentaArrakisVault) { - throw new Error(sdkErrors.UNSUPPORTED_NETWORK); - } - - const [ - balance, - earned, - periodFinish, - rewardRate, - totalSupply, - lpTokenBalance, - allowance, - ] = await Promise.all([ - StakingRewards.balanceOf(this.sdk.context.walletAddress), - StakingRewards.earned(this.sdk.context.walletAddress), - StakingRewards.periodFinish(), - StakingRewards.rewardRate(), - StakingRewards.totalSupply(), - KwentaArrakisVault.balanceOf(this.sdk.context.walletAddress), - KwentaArrakisVault.allowance(this.sdk.context.walletAddress, StakingRewards.address), - ]); - - return { - balance: wei(balance), - earned: wei(earned), - endDate: periodFinish.toNumber(), - rewardRate: wei(rewardRate), - totalSupply: wei(totalSupply), - lpTokenBalance: wei(lpTokenBalance), - allowance: wei(allowance), - }; - } - - public async claimRewards() { - const StakingRewards = this.sdk.context.contracts.StakingRewards; - - if (!StakingRewards) { - throw new Error(sdkErrors.UNSUPPORTED_NETWORK); - } - - const { hash } = await this.sdk.transactions.createContractTxn(StakingRewards, 'getReward', []); - - return hash; - } -} diff --git a/sections/dashboard/Stake/EscrowTable.tsx b/sections/dashboard/Stake/EscrowTable.tsx index 80f23e8ab7..50644ad7c6 100644 --- a/sections/dashboard/Stake/EscrowTable.tsx +++ b/sections/dashboard/Stake/EscrowTable.tsx @@ -1,30 +1,29 @@ import { useCallback, useMemo, useState } from 'react'; -import React from 'react'; import { useTranslation } from 'react-i18next'; import { CellProps } from 'react-table'; import styled from 'styled-components'; -import { useContractWrite, usePrepareContractWrite } from 'wagmi'; import { DesktopOnlyView, MobileOrTabletView } from 'components/Media'; import Table from 'components/Table'; import { TableCellHead } from 'components/Table/Table'; -import { monitorTransaction } from 'contexts/RelayerContext'; -import { useStakingContext } from 'contexts/StakingContext'; -import { EscrowRow } from 'hooks/useStakingData'; -import { truncateNumbers } from 'utils/formatters/number'; +import type { EscrowData } from 'sdk/services/kwentaToken'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; +import { vestEscrowedRewards } from 'state/staking/actions'; +import { truncateNumbers, zeroBN } from 'utils/formatters/number'; import { StakingCard } from './common'; import VestConfirmationModal from './VestConfirmationModal'; const EscrowTable = () => { const { t } = useTranslation(); - const { escrowRows, rewardEscrowContract } = useStakingContext(); - const [checkedState, setCheckedState] = useState(escrowRows.map((_) => false)); + const dispatch = useAppDispatch(); + const escrowData = useAppSelector(({ staking }) => staking.escrowData); + const [checkedState, setCheckedState] = useState(escrowData.map((_) => false)); const [checkAllState, setCheckAllState] = useState(false); - const [openConfirmModal, setOpenConfirmModal] = useState(false); + const [isConfirmModalOpen, setConfirmModalOpen] = useState(false); const handleOnChange = useCallback( - (position: number) => { + (position: number) => () => { checkedState[position] = !checkedState[position]; setCheckedState([...checkedState]); }, @@ -33,77 +32,68 @@ const EscrowTable = () => { const selectAll = useCallback(() => { if (checkAllState) { - setCheckedState(escrowRows.map((_) => false)); + setCheckedState(escrowData.map((_) => false)); setCheckAllState(false); } else { - setCheckedState(escrowRows.map((_) => true)); + setCheckedState(escrowData.map((_) => true)); setCheckAllState(true); } - }, [checkAllState, escrowRows]); + }, [checkAllState, escrowData]); const columnsDeps = useMemo(() => [checkedState], [checkedState]); - const totalVestable = useMemo( + const { totalVestable, totalFee } = useMemo( () => - checkedState.reduce((acc, current, index) => { - if (current === true) { - return acc + escrowRows[index]?.vestable ?? 0; - } - return acc; - }, 0), - [checkedState, escrowRows] - ); + checkedState.reduce( + (acc, current, index) => { + if (current) { + acc.totalVestable = acc.totalVestable.add(escrowData[index].vestable); + acc.totalFee = acc.totalFee.add(escrowData[index].fee); + } - const totalFee = useMemo( - () => - checkedState.reduce((acc, current, index) => { - if (current === true) { - return acc + escrowRows[index]?.fee ?? 0; - } - return acc; - }, 0), - [checkedState, escrowRows] + return acc; + }, + { totalVestable: zeroBN, totalFee: zeroBN } + ), + [checkedState, escrowData] ); - const { config } = usePrepareContractWrite({ - ...rewardEscrowContract, - functionName: 'vest', - args: [escrowRows.filter((d, index) => !!checkedState[index]).map((d) => d.id)], - enabled: escrowRows.filter((d, index) => !!checkedState[index]).map((d) => d.id).length > 0, - }); + const { ids, vestEnabled } = useMemo(() => { + const ids = escrowData.filter((_, i) => !!checkedState[i]).map((d) => d.id); + const vestEnabled = ids.length > 0; - const { writeAsync: vest } = useContractWrite(config); + return { ids, vestEnabled }; + }, [escrowData, checkedState]); const handleVest = useCallback(async () => { - const tx = await vest?.(); - setOpenConfirmModal(false); - monitorTransaction({ - txHash: tx?.hash ?? '', - onTxConfirmed: () => { - setCheckedState(escrowRows.map((_) => false)); - setCheckAllState(false); - }, - }); - }, [escrowRows, vest]); + if (vestEnabled) { + await dispatch(vestEscrowedRewards(ids)); + setCheckedState(escrowData.map((_) => false)); + setCheckAllState(false); + } + + setConfirmModalOpen(false); + }, [dispatch, escrowData, ids, vestEnabled]); + + const openConfirmModal = useCallback(() => setConfirmModalOpen(true), []); + const closeConfirmModal = useCallback(() => setConfirmModalOpen(false), []); return ( ( - selectAll()} /> - ), - Cell: (cellProps: CellProps) => ( + Header: () => , + Cell: (cellProps: CellProps) => ( handleOnChange(cellProps.row.index)} + onChange={handleOnChange(cellProps.row.index)} /> ), accessor: 'selected', @@ -111,7 +101,7 @@ const EscrowTable = () => { }, { Header: () => {t('dashboard.stake.tabs.escrow.date')}, - Cell: (cellProps: CellProps) => ( + Cell: (cellProps: CellProps) => ( {cellProps.row.original.date} ), accessor: 'date', @@ -123,7 +113,7 @@ const EscrowTable = () => {
{t('dashboard.stake.tabs.escrow.time-until-vestable')}
), - Cell: (cellProps: CellProps) => ( + Cell: (cellProps: CellProps) => ( {cellProps.row.original.time} ), accessor: 'timeUntilVestable', @@ -135,7 +125,7 @@ const EscrowTable = () => {
{t('dashboard.stake.tabs.escrow.immediately-vestable')}
), - Cell: (cellProps: CellProps) => ( + Cell: (cellProps: CellProps) => ( {truncateNumbers(cellProps.row.original.vestable, 4)} ), accessor: 'immediatelyVestable', @@ -143,7 +133,7 @@ const EscrowTable = () => { }, { Header: () => {t('dashboard.stake.tabs.escrow.amount')}, - Cell: (cellProps: CellProps) => ( + Cell: (cellProps: CellProps) => ( {truncateNumbers(cellProps.row.original.amount, 4)} ), accessor: 'amount', @@ -155,7 +145,7 @@ const EscrowTable = () => {
{t('dashboard.stake.tabs.escrow.early-vest-fee')}
), - Cell: (cellProps: CellProps) => ( + Cell: (cellProps: CellProps) => ( {truncateNumbers(cellProps.row.original.fee, 4)} ), accessor: 'earlyVestFee', @@ -163,7 +153,7 @@ const EscrowTable = () => { }, { Header: () => {t('dashboard.stake.tabs.escrow.status')}, - Cell: (cellProps: CellProps) => ( + Cell: (cellProps: CellProps) => ( {cellProps.row.original.status} ), accessor: 'status', @@ -174,19 +164,17 @@ const EscrowTable = () => {
( - selectAll()} /> - ), - Cell: (cellProps: CellProps) => ( + Header: () => , + Cell: (cellProps: CellProps) => ( handleOnChange(cellProps.row.index)} + onChange={handleOnChange(cellProps.row.index)} /> ), accessor: 'selected', @@ -194,7 +182,7 @@ const EscrowTable = () => { }, { Header: () => {t('dashboard.stake.tabs.escrow.amount')}, - Cell: (cellProps: CellProps) => ( + Cell: (cellProps: CellProps) => ( {truncateNumbers(cellProps.row.original.amount, 4)} ), accessor: 'amount', @@ -204,7 +192,7 @@ const EscrowTable = () => { Header: () => ( {t('dashboard.stake.tabs.escrow.early-vest-fee')} ), - Cell: (cellProps: CellProps) => ( + Cell: (cellProps: CellProps) => ( {truncateNumbers(cellProps.row.original.fee, 4)} ), accessor: 'earlyVestFee', @@ -212,7 +200,7 @@ const EscrowTable = () => { }, { Header: () => {t('dashboard.stake.tabs.escrow.status')}, - Cell: (cellProps: CellProps) => ( + Cell: (cellProps: CellProps) => ( {cellProps.row.original.status} ), accessor: 'status', @@ -226,26 +214,25 @@ const EscrowTable = () => {
{t('dashboard.stake.tabs.escrow.total')}
- {truncateNumbers(totalVestable ?? 0, 4)}{' '} + {truncateNumbers(totalVestable, 4)}{' '} {t('dashboard.stake.tabs.stake-table.kwenta-token')}
{t('dashboard.stake.tabs.escrow.fee')}
- {truncateNumbers(totalFee ?? 0, 4)}{' '} - {t('dashboard.stake.tabs.stake-table.kwenta-token')} + {truncateNumbers(totalFee, 4)} {t('dashboard.stake.tabs.stake-table.kwenta-token')}
- setOpenConfirmModal(true)}> + {t('dashboard.stake.tabs.escrow.vest')} - {openConfirmModal && ( + {isConfirmModalOpen && ( setOpenConfirmModal(false)} + onDismiss={closeConfirmModal} handleVest={handleVest} /> )} @@ -311,7 +298,7 @@ const EscrowStats = styled.div` } `; -const VestButton = styled.button<{ disabled: boolean }>` +const VestButton = styled.button` border-width: 1px; border-style: solid; border-color: ${(props) => diff --git a/sections/dashboard/Stake/InputCards/EscrowInputCard.tsx b/sections/dashboard/Stake/InputCards/EscrowInputCard.tsx index 4058d6ef92..c0323570ed 100644 --- a/sections/dashboard/Stake/InputCards/EscrowInputCard.tsx +++ b/sections/dashboard/Stake/InputCards/EscrowInputCard.tsx @@ -1,113 +1,129 @@ import { wei } from '@synthetixio/wei'; -import isNil from 'lodash/isNil'; import { FC, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import { useContractWrite, usePrepareContractWrite } from 'wagmi'; import Button from 'components/Button'; import NumericInput from 'components/Input/NumericInput'; import SegmentedControl from 'components/SegmentedControl'; import { DEFAULT_CRYPTO_DECIMALS, DEFAULT_TOKEN_DECIMALS } from 'constants/defaults'; -import { monitorTransaction } from 'contexts/RelayerContext'; -import { useStakingContext } from 'contexts/StakingContext'; -import { STAKING_LOW_GAS_LIMIT } from 'queries/staking/utils'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; +import { approveKwentaToken, stakeEscrow, unstakeEscrow } from 'state/staking/actions'; +import { + selectEscrowedKwentaBalance, + selectIsKwentaTokenApproved, + selectIsStakingEscrowedKwenta, + selectIsUnstakingEscrowedKwenta, + selectStakedEscrowedKwentaBalance, +} from 'state/staking/selectors'; import { FlexDivRowCentered, numericValueCSS } from 'styles/common'; -import { truncateNumbers, zeroBN } from 'utils/formatters/number'; +import { toWei, truncateNumbers, zeroBN } from 'utils/formatters/number'; import { StakingCard } from '../common'; const EscrowInputCard: FC = () => { const { t } = useTranslation(); - const { - stakedEscrowedBalance, - escrowedBalance, - kwentaApproveConfig, - kwentaTokenApproval, - rewardEscrowContract, - } = useStakingContext(); + const dispatch = useAppDispatch(); const [amount, setAmount] = useState(''); const [activeTab, setActiveTab] = useState(0); - const amountBN = useMemo( - () => (amount === '' ? zeroBN.toString(0, true) : wei(amount).toString(0, true)), - [amount] - ); + const escrowedKwentaBalance = useAppSelector(selectEscrowedKwentaBalance); + const stakedEscrowedKwentaBalance = useAppSelector(selectStakedEscrowedKwentaBalance); + const isKwentaTokenApproved = useAppSelector(selectIsKwentaTokenApproved); + + const isStakingEscrowedKwenta = useAppSelector(selectIsStakingEscrowedKwenta); + const isUnstakingEscrowedKwenta = useAppSelector(selectIsUnstakingEscrowedKwenta); + + const amountBN = useMemo(() => toWei(amount).toBN(), [amount]); const unstakedEscrowedKwentaBalance = useMemo( () => - !isNil(escrowedBalance) && escrowedBalance.gt(0) - ? escrowedBalance.sub(stakedEscrowedBalance ?? zeroBN) ?? zeroBN - : zeroBN, - [escrowedBalance, stakedEscrowedBalance] + escrowedKwentaBalance.gt(0) ? escrowedKwentaBalance.sub(stakedEscrowedKwentaBalance) : zeroBN, + [escrowedKwentaBalance, stakedEscrowedKwentaBalance] ); - const handleTabChange = useCallback((tabIndex: number) => { - setAmount(''); - setActiveTab(tabIndex); - }, []); - const maxBalance = useMemo( - () => - activeTab === 0 - ? wei(unstakedEscrowedKwentaBalance ?? zeroBN) - : wei(stakedEscrowedBalance ?? zeroBN), - [activeTab, stakedEscrowedBalance, unstakedEscrowedKwentaBalance] + () => (activeTab === 0 ? wei(unstakedEscrowedKwentaBalance) : wei(stakedEscrowedKwentaBalance)), + [activeTab, stakedEscrowedKwentaBalance, unstakedEscrowedKwentaBalance] ); - const onMaxClick = useCallback(async () => { + const stakeEnabled = useMemo(() => { + return ( + activeTab === 0 && + unstakedEscrowedKwentaBalance.gt(0) && + !!parseFloat(amount) && + !isStakingEscrowedKwenta + ); + }, [activeTab, unstakedEscrowedKwentaBalance, amount, isStakingEscrowedKwenta]); + + const unstakeEnabled = useMemo(() => { + return ( + activeTab === 1 && + stakedEscrowedKwentaBalance.gt(0) && + !!parseFloat(amount) && + !isUnstakingEscrowedKwenta + ); + }, [activeTab, stakedEscrowedKwentaBalance, amount, isUnstakingEscrowedKwenta]); + + const handleApprove = useCallback(() => { + dispatch(approveKwentaToken('kwenta')); + }, [dispatch]); + + const handleStakeEscrow = useCallback(() => { + dispatch(stakeEscrow(amountBN)); + }, [dispatch, amountBN]); + + const handleUnstakeEscrow = useCallback(() => { + dispatch(unstakeEscrow(amountBN)); + }, [dispatch, amountBN]); + + const onMaxClick = useCallback(() => { setAmount(truncateNumbers(maxBalance, DEFAULT_TOKEN_DECIMALS)); }, [maxBalance]); - const { config: stakedEscrowKwentaConfig } = usePrepareContractWrite({ - ...rewardEscrowContract, - functionName: 'stakeEscrow', - args: [amountBN], - overrides: { - gasLimit: STAKING_LOW_GAS_LIMIT, - }, - enabled: activeTab === 0 && unstakedEscrowedKwentaBalance.gt(0) && !!parseFloat(amount), - }); - - const { config: unstakedEscrowKwentaConfig } = usePrepareContractWrite({ - ...rewardEscrowContract, - functionName: 'unstakeEscrow', - args: [amountBN], - overrides: { - gasLimit: STAKING_LOW_GAS_LIMIT, - }, - enabled: activeTab === 1 && stakedEscrowedBalance.gt(0) && !!parseFloat(amount), - }); - - const { writeAsync: kwentaApprove } = useContractWrite(kwentaApproveConfig); - const { writeAsync: stakeEscrowKwenta } = useContractWrite(stakedEscrowKwentaConfig); - const { writeAsync: unstakeEscrowKwenta } = useContractWrite(unstakedEscrowKwentaConfig); + const handleTabChange = useCallback((tabIndex: number) => { + setAmount(''); + setActiveTab(tabIndex); + }, []); + + const isDisabled = useMemo(() => { + if (!isKwentaTokenApproved) { + return false; + } else { + return activeTab === 0 ? !stakeEnabled : !unstakeEnabled; + } + }, [isKwentaTokenApproved, activeTab, stakeEnabled, unstakeEnabled]); const submitEscrow = useCallback(async () => { - if (kwentaTokenApproval) { - const approveTxn = await kwentaApprove?.(); - monitorTransaction({ - txHash: approveTxn?.hash ?? '', - }); - } else if (activeTab === 0) { - const stakeTxn = await stakeEscrowKwenta?.(); - monitorTransaction({ - txHash: stakeTxn?.hash ?? '', - onTxConfirmed: () => { - setAmount(''); - }, - }); + if (!isKwentaTokenApproved) { + handleApprove(); + } else if (stakeEnabled) { + handleStakeEscrow(); + } else if (unstakeEnabled) { + handleUnstakeEscrow(); + } + }, [ + handleApprove, + isKwentaTokenApproved, + handleStakeEscrow, + handleUnstakeEscrow, + stakeEnabled, + unstakeEnabled, + ]); + + const balance = useMemo(() => { + const b = activeTab === 0 ? unstakedEscrowedKwentaBalance : stakedEscrowedKwentaBalance; + return truncateNumbers(b, DEFAULT_CRYPTO_DECIMALS); + }, [activeTab, unstakedEscrowedKwentaBalance, stakedEscrowedKwentaBalance]); + + const handleChange = useCallback((_: any, newValue: string) => { + if (newValue !== '' && newValue.indexOf('.') === -1) { + setAmount(parseFloat(newValue).toString()); } else { - const unstakeTxn = await unstakeEscrowKwenta?.(); - monitorTransaction({ - txHash: unstakeTxn?.hash ?? '', - onTxConfirmed: () => { - setAmount(''); - }, - }); + setAmount(newValue); } - }, [activeTab, kwentaApprove, kwentaTokenApproval, stakeEscrowKwenta, unstakeEscrowKwenta]); + }, []); return ( @@ -118,7 +134,6 @@ const EscrowInputCard: FC = () => { ]} onChange={handleTabChange} selectedIndex={activeTab} - style={{ marginBottom: '20px' }} /> @@ -126,36 +141,14 @@ const EscrowInputCard: FC = () => {
{t('dashboard.stake.tabs.stake-table.balance')}
- {activeTab === 0 - ? truncateNumbers(unstakedEscrowedKwentaBalance, DEFAULT_CRYPTO_DECIMALS) - : truncateNumbers(stakedEscrowedBalance, DEFAULT_CRYPTO_DECIMALS)} + {balance}
- { - newValue !== '' && newValue.indexOf('.') === -1 - ? setAmount(parseFloat(newValue).toString()) - : setAmount(newValue); - }} - /> +
-
); diff --git a/sections/dashboard/Stake/InputCards/StakeInputCard.tsx b/sections/dashboard/Stake/InputCards/StakeInputCard.tsx index a3d47d7c28..17c9d65601 100644 --- a/sections/dashboard/Stake/InputCards/StakeInputCard.tsx +++ b/sections/dashboard/Stake/InputCards/StakeInputCard.tsx @@ -1,102 +1,109 @@ -import { wei } from '@synthetixio/wei'; import _ from 'lodash'; import { FC, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import { useContractWrite, usePrepareContractWrite } from 'wagmi'; import Button from 'components/Button'; import NumericInput from 'components/Input/NumericInput'; import SegmentedControl from 'components/SegmentedControl'; import { DEFAULT_CRYPTO_DECIMALS, DEFAULT_TOKEN_DECIMALS } from 'constants/defaults'; -import { monitorTransaction } from 'contexts/RelayerContext'; -import { useStakingContext } from 'contexts/StakingContext'; -import { STAKING_LOW_GAS_LIMIT } from 'queries/staking/utils'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; +import { approveKwentaToken } from 'state/staking/actions'; +import { stakeKwenta, unstakeKwenta } from 'state/staking/actions'; +import { + selectIsKwentaTokenApproved, + selectIsStakingKwenta, + selectIsUnstakingKwenta, + selectKwentaBalance, + selectStakedKwentaBalance, +} from 'state/staking/selectors'; import { FlexDivRowCentered, numericValueCSS } from 'styles/common'; -import { truncateNumbers, zeroBN } from 'utils/formatters/number'; +import { toWei, truncateNumbers } from 'utils/formatters/number'; import { StakingCard } from '../common'; const StakeInputCard: FC = () => { const { t } = useTranslation(); - const { - kwentaBalance, - stakedNonEscrowedBalance, - kwentaApproveConfig, - kwentaTokenApproval, - stakingRewardsContract, - } = useStakingContext(); + const dispatch = useAppDispatch(); + + const kwentaBalance = useAppSelector(selectKwentaBalance); + const stakedKwentaBalance = useAppSelector(selectStakedKwentaBalance); const [amount, setAmount] = useState(''); const [activeTab, setActiveTab] = useState(0); - const amountBN = useMemo( - () => (amount === '' ? zeroBN.toString(0, true) : wei(amount).toString(0, true)), - [amount] - ); + const amountBN = useMemo(() => toWei(amount).toBN(), [amount]); - const handleTabChange = (tabIndex: number) => { + const handleTabChange = useCallback((tabIndex: number) => { setAmount(''); setActiveTab(tabIndex); - }; + }, []); - const maxBalance = useMemo( - () => - activeTab === 0 ? wei(kwentaBalance ?? zeroBN) : wei(stakedNonEscrowedBalance ?? zeroBN), - [activeTab, kwentaBalance, stakedNonEscrowedBalance] - ); + const maxBalance = useMemo(() => (activeTab === 0 ? kwentaBalance : stakedKwentaBalance), [ + activeTab, + kwentaBalance, + stakedKwentaBalance, + ]); const onMaxClick = useCallback(() => { setAmount(truncateNumbers(maxBalance, DEFAULT_TOKEN_DECIMALS)); }, [maxBalance]); - const { config: stakeKwentaConfig } = usePrepareContractWrite({ - ...stakingRewardsContract, - functionName: 'stake', - args: [amountBN], - overrides: { - gasLimit: STAKING_LOW_GAS_LIMIT, - }, - enabled: activeTab === 0 && kwentaBalance.gt(0) && !!parseFloat(amount), - }); - - const { config: unstakeKwentaConfig } = usePrepareContractWrite({ - ...stakingRewardsContract, - functionName: 'unstake', - args: [amountBN], - overrides: { - gasLimit: STAKING_LOW_GAS_LIMIT, - }, - enabled: activeTab === 1 && stakedNonEscrowedBalance.gt(0) && !!parseFloat(amount), - }); - - const { writeAsync: kwentaApprove } = useContractWrite(kwentaApproveConfig); - const { writeAsync: stakeKwenta } = useContractWrite(stakeKwentaConfig); - const { writeAsync: unstakeKwenta } = useContractWrite(unstakeKwentaConfig); + const isKwentaTokenApproved = useAppSelector(selectIsKwentaTokenApproved); + const isStakingKwenta = useAppSelector(selectIsStakingKwenta); + const isUnstakingKwenta = useAppSelector(selectIsUnstakingKwenta); + + const handleApprove = useCallback(() => { + dispatch(approveKwentaToken('kwenta')); + }, [dispatch]); + + const handleStakeKwenta = useCallback(() => { + dispatch(stakeKwenta(amountBN)); + }, [dispatch, amountBN]); + + const handleUnstakeKwenta = useCallback(() => { + dispatch(unstakeKwenta(amountBN)); + }, [dispatch, amountBN]); + + const stakeEnabled = useMemo(() => { + return activeTab === 0 && kwentaBalance.gt(0) && !!parseFloat(amount) && !isStakingKwenta; + }, [activeTab, kwentaBalance, amount, isStakingKwenta]); + + const unstakeEnabled = useMemo(() => { + return ( + activeTab === 1 && stakedKwentaBalance.gt(0) && !!parseFloat(amount) && !isUnstakingKwenta + ); + }, [activeTab, stakedKwentaBalance, amount, isUnstakingKwenta]); + + const isDisabled = useMemo(() => { + if (!isKwentaTokenApproved) { + return false; + } else { + return activeTab === 0 ? !stakeEnabled : !unstakeEnabled; + } + }, [isKwentaTokenApproved, activeTab, stakeEnabled, unstakeEnabled]); const submitStake = useCallback(async () => { - if (kwentaTokenApproval) { - const approveTxn = await kwentaApprove?.(); - monitorTransaction({ - txHash: approveTxn?.hash ?? '', - }); + if (!isKwentaTokenApproved) { + handleApprove(); } else if (activeTab === 0) { - const stakeTxn = await stakeKwenta?.(); - monitorTransaction({ - txHash: stakeTxn?.hash ?? '', - onTxConfirmed: () => { - setAmount(''); - }, - }); + handleStakeKwenta(); } else { - const unstakeTxn = await unstakeKwenta?.(); - monitorTransaction({ - txHash: unstakeTxn?.hash ?? '', - onTxConfirmed: () => { - setAmount(''); - }, - }); + handleUnstakeKwenta(); } - }, [activeTab, kwentaApprove, kwentaTokenApproval, stakeKwenta, unstakeKwenta]); + }, [activeTab, handleApprove, isKwentaTokenApproved, handleStakeKwenta, handleUnstakeKwenta]); + + const handleChange = useCallback((_: any, value: string) => { + if (value !== '' && value.indexOf('.') === -1) { + setAmount(parseFloat(value).toString()); + } else { + setAmount(value); + } + }, []); + + const balance = useMemo(() => { + const b = activeTab === 0 ? kwentaBalance : stakedKwentaBalance; + return truncateNumbers(b, DEFAULT_CRYPTO_DECIMALS); + }, [activeTab, kwentaBalance, stakedKwentaBalance]); return ( @@ -115,32 +122,14 @@ const StakeInputCard: FC = () => {
{t('dashboard.stake.tabs.stake-table.balance')}
- {activeTab === 0 - ? truncateNumbers(kwentaBalance, DEFAULT_CRYPTO_DECIMALS) - : truncateNumbers(stakedNonEscrowedBalance, DEFAULT_CRYPTO_DECIMALS)} + {balance}
- { - newValue !== '' && newValue.indexOf('.') === -1 - ? setAmount(parseFloat(newValue).toString()) - : setAmount(newValue); - }} - /> + - diff --git a/sections/dashboard/Stake/StakingTabs.tsx b/sections/dashboard/Stake/StakingTabs.tsx index 4366d71002..7c3a889e2b 100644 --- a/sections/dashboard/Stake/StakingTabs.tsx +++ b/sections/dashboard/Stake/StakingTabs.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useMemo, useEffect } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -7,13 +7,12 @@ import LabelContainer from 'components/Nav/DropDownLabel'; import Select from 'components/Select'; import { DropdownIndicator, IndicatorSeparator } from 'components/Select/Select'; import { TabPanel } from 'components/Tab'; -import Connector from 'containers/Connector'; -import { useStakingContext } from 'contexts/StakingContext'; import useIsL2 from 'hooks/useIsL2'; -import { getEpochDetails } from 'queries/staking/utils'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; +import { setSelectedEpoch } from 'state/staking/reducer'; +import { selectEpochData, selectSelectedEpoch } from 'state/staking/selectors'; import { FlexDivRowCentered } from 'styles/common'; import media from 'styles/media'; -import { formatShortDate, toJSTimestamp } from 'utils/formatters/date'; import EscrowTab from './EscrowTab'; import RedemptionTab from './RedemptionTab'; @@ -21,77 +20,41 @@ import { StakeTab } from './StakingPortfolio'; import StakingTab from './StakingTab'; import TradingRewardsTab from './TradingRewardsTab'; -type EpochLabel = { +type EpochValue = { period: number; start: number; end: number; - startDate: string; - endDate: string; label: string; }; type StakingTabsProp = { - currentTab?: StakeTab; + currentTab: StakeTab; + onChangeTab(tab: StakeTab): () => void; }; -const StakingTabs: React.FC = ({ currentTab }) => { +const StakingTabs: React.FC = ({ currentTab, onChangeTab }) => { const { t } = useTranslation(); - const { network } = Connector.useContainer(); const isL2 = useIsL2(); - const { epochPeriod, periods } = useStakingContext(); + const dispatch = useAppDispatch(); - const [period, setPeriod] = useState(epochPeriod); - const [start, setStart] = useState(0); - const [end, setEnd] = useState(0); - const [currentEpochLabel, setCurrentEpochLabel] = useState( - `Epoch 1: Oct 23, 2022 - Oct 30, 2022` + const epochData = useAppSelector(selectEpochData); + const selectedEpoch = useAppSelector(selectSelectedEpoch); + + const handleChangeEpoch = useCallback( + (value: EpochValue) => () => { + dispatch(setSelectedEpoch(value.period)); + }, + [dispatch] ); - const [activeTab, setActiveTab] = useState(StakeTab.Staking); - - useEffect(() => { - setActiveTab(currentTab !== undefined ? currentTab : StakeTab.Staking); - }, [currentTab]); - - const handleTabSwitch = useCallback((tab: StakeTab) => () => setActiveTab(tab), []); - - const epochData = useMemo(() => { - const epochData = periods.map((i) => { - const { epochStart, epochEnd } = getEpochDetails(network?.id, i); - const startDate = formatShortDate(new Date(toJSTimestamp(epochStart))); - const endDate = formatShortDate(new Date(toJSTimestamp(epochEnd))); - const label = `Epoch ${i}: ${startDate} - ${endDate}`; - - setPeriod(i); - setStart(epochStart ?? 0); - setEnd(epochEnd ?? 0); - setCurrentEpochLabel(label); - - return { - period: i, - start: epochStart, - end: epochEnd, - startDate, - endDate, - label, - }; - }); - return epochData ?? []; - }, [network?.id, periods]); - - const formatOptionLabel = ({ label, start, end, period }: EpochLabel) => { - return ( -
{ - setPeriod(period); - setStart(start ?? 0); - setEnd(end ?? 0); - setCurrentEpochLabel(label); - }} - > - {label} + + const formatOptionLabel = useCallback( + (option: EpochValue) => ( +
+ {option.label}
- ); - }; + ), + [handleChangeEpoch] + ); return ( @@ -99,13 +62,13 @@ const StakingTabs: React.FC = ({ currentTab }) => { = ({ currentTab }) => { ? t('dashboard.stake.tabs.trading-rewards.title') : t('dashboard.stake.tabs.trading-rewards.mobile-title') } - onClick={handleTabSwitch(StakeTab.TradingRewards)} - active={activeTab === StakeTab.TradingRewards} + onClick={onChangeTab(StakeTab.TradingRewards)} + active={currentTab === StakeTab.TradingRewards} /> - + {window.innerWidth < 768 && ( {t('dashboard.stake.tabs.staking.current-trading-period')} )} @@ -132,12 +95,7 @@ const StakingTabs: React.FC = ({ currentTab }) => { controlHeight={41} options={epochData.sort((a, b) => b.period - a.period)} optionPadding={'0px'} - value={{ - label: currentEpochLabel, - period, - start, - end, - }} + value={selectedEpoch} menuWidth={240} components={{ IndicatorSeparator, DropdownIndicator }} isSearchable={false} @@ -148,16 +106,20 @@ const StakingTabs: React.FC = ({ currentTab }) => {
- + - - + + - + - +
@@ -178,12 +140,18 @@ const StakingSelect = styled(Select)` border-radius: 20px; background: ${(props) => props.theme.colors.selectedTheme.surfaceFill}; } + .react-select__value-container { padding: 0; } + .react-select__single-value > div > div { font-size: 12px; } + + .react-select__dropdown-indicator { + margin-right: 10px; + } `; const StyledFlexDivRowCentered = styled(FlexDivRowCentered)<{ active: boolean }>` diff --git a/sections/dashboard/Stake/TradingRewardsTab.tsx b/sections/dashboard/Stake/TradingRewardsTab.tsx index ac333e6c59..0246577a32 100644 --- a/sections/dashboard/Stake/TradingRewardsTab.tsx +++ b/sections/dashboard/Stake/TradingRewardsTab.tsx @@ -1,26 +1,23 @@ import { wei } from '@synthetixio/wei'; import { formatEther } from 'ethers/lib/utils.js'; -import { useMemo } from 'react'; +import { useCallback, useEffect, useMemo, FC } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import { useContractReads, useContractWrite, usePrepareContractWrite } from 'wagmi'; import HelpIcon from 'assets/svg/app/question-mark.svg'; import Button from 'components/Button'; import StyledTooltip from 'components/Tooltip/StyledTooltip'; import Connector from 'containers/Connector'; -import { monitorTransaction } from 'contexts/RelayerContext'; -import { useStakingContext } from 'contexts/StakingContext'; -import useGetFiles from 'queries/files/useGetFiles'; import useGetFuturesFee from 'queries/staking/useGetFuturesFee'; import useGetFuturesFeeForAccount from 'queries/staking/useGetFuturesFeeForAccount'; import { - ClaimParams, - EpochDataProps, FuturesFeeForAccountProps, FuturesFeeProps, TradingRewardProps, } from 'queries/staking/utils'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; +import { claimMultipleRewards, fetchClaimableRewards } from 'state/staking/actions'; +import { selectResetTime, selectTotalRewards } from 'state/staking/selectors'; import { FlexDivRow } from 'styles/common'; import media from 'styles/media'; import { formatTruncatedDuration } from 'utils/formatters/date'; @@ -28,95 +25,45 @@ import { formatDollars, formatPercent, truncateNumbers, zeroBN } from 'utils/for import { KwentaLabel, StakingCard } from './common'; -const TradingRewardsTab: React.FC = ({ +const TradingRewardsTab: FC = ({ period = 0, start = 0, end = Math.floor(Date.now() / 1000), -}: TradingRewardProps) => { +}) => { const { t } = useTranslation(); const { walletAddress } = Connector.useContainer(); - const { multipleMerkleDistributorContract, periods, resetTime } = useStakingContext(); + const dispatch = useAppDispatch(); - const allEpochQuery = useGetFiles(periods); - const allEpochData = useMemo(() => allEpochQuery?.data ?? [], [allEpochQuery?.data]); - - // eslint-disable-next-line react-hooks/exhaustive-deps - let rewards: ClaimParams[] = []; - - allEpochData && - allEpochData.length > 0 && - allEpochData.forEach((d: EpochDataProps, period) => { - const index = Object.keys(d.claims).findIndex((key) => key === walletAddress); - if (index !== -1) { - const walletReward = Object.values(d.claims)[index]; - if (!!walletReward && walletAddress != null) { - rewards.push([ - walletReward?.index, - walletAddress, - walletReward?.amount, - walletReward?.proof, - period, - ]); - } - } - }); - - const checkIsClaimed = useMemo(() => { - return rewards.map((reward: ClaimParams) => { - return { - ...multipleMerkleDistributorContract, - functionName: 'isClaimed', - args: [reward[0], reward[4]], - }; - }); - }, [multipleMerkleDistributorContract, rewards]); - - const { data: isClaimable } = useContractReads({ - contracts: checkIsClaimed, - enabled: checkIsClaimed && checkIsClaimed.length > 0, - watch: true, - scopeKey: 'staking', - }); - - const claimableRewards = useMemo( - () => - isClaimable && isClaimable.length > 0 - ? rewards.filter((_, index) => !isClaimable[index]) - : [], - [isClaimable, rewards] - ); - - const totalRewards = - claimableRewards.length > 0 - ? claimableRewards.reduce((acc, curr) => wei(acc).add(formatEther(curr[2])), zeroBN) - : 0; + const resetTime = useAppSelector(selectResetTime); + const totalRewards = useAppSelector(selectTotalRewards); const futuresFeeQuery = useGetFuturesFeeForAccount(walletAddress!, start, end); const futuresFeePaid = useMemo(() => { - const t = futuresFeeQuery.data ?? []; + const t: FuturesFeeForAccountProps[] = futuresFeeQuery.data ?? []; return t - .map((trade: FuturesFeeForAccountProps) => formatEther(trade.feesPaid.toString())) - .reduce((acc: number, curr: number) => wei(acc).add(wei(curr)), zeroBN); + .map((trade) => formatEther(trade.feesPaid.toString())) + .reduce((acc, curr) => acc.add(wei(curr)), zeroBN); }, [futuresFeeQuery.data]); const totalFuturesFeeQuery = useGetFuturesFee(start, end); const totalFuturesFeePaid = useMemo(() => { - const t = totalFuturesFeeQuery.data ?? []; + const t: FuturesFeeProps[] = totalFuturesFeeQuery.data ?? []; return t - .map((trade: FuturesFeeProps) => formatEther(trade.feesCrossMarginAccounts.toString())) - .reduce((acc: number, curr: number) => wei(acc).add(wei(curr)), zeroBN); + .map((trade) => formatEther(trade.feesCrossMarginAccounts.toString())) + .reduce((acc, curr) => acc.add(wei(curr)), zeroBN); }, [totalFuturesFeeQuery.data]); - const { config } = usePrepareContractWrite({ - ...multipleMerkleDistributorContract, - functionName: 'claimMultiple', - args: [claimableRewards], - enabled: claimableRewards && claimableRewards.length > 0, - }); + const claimDisabled = useMemo(() => totalRewards.lte(0), [totalRewards]); - const { writeAsync: claim } = useContractWrite(config); + useEffect(() => { + dispatch(fetchClaimableRewards()); + }, [dispatch]); + + const handleClaim = useCallback(() => { + dispatch(claimMultipleRewards()); + }, [dispatch]); const ratio = useMemo( () => @@ -132,7 +79,7 @@ const TradingRewardsTab: React.FC = ({
{t('dashboard.stake.tabs.trading-rewards.claimable-rewards-all')}
- {truncateNumbers(wei(totalRewards) ?? zeroBN, 4)} + {truncateNumbers(totalRewards, 4)}
@@ -146,18 +93,7 @@ const TradingRewardsTab: React.FC = ({
- diff --git a/sections/dashboard/Stake/VestConfirmationModal.tsx b/sections/dashboard/Stake/VestConfirmationModal.tsx index 4ba87b8692..4387ab80ad 100644 --- a/sections/dashboard/Stake/VestConfirmationModal.tsx +++ b/sections/dashboard/Stake/VestConfirmationModal.tsx @@ -1,3 +1,4 @@ +import Wei from '@synthetixio/wei'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -11,7 +12,7 @@ import { truncateNumbers } from 'utils/formatters/number'; type Props = { onDismiss(): void; - totalFee: number; + totalFee: Wei; handleVest(): void; }; diff --git a/state/earn/actions.ts b/state/earn/actions.ts index 4e114adc70..9a5fb47b9e 100644 --- a/state/earn/actions.ts +++ b/state/earn/actions.ts @@ -3,10 +3,10 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { monitorTransaction } from 'contexts/RelayerContext'; import { ThunkConfig } from 'state/types'; -export const approveLPToken = createAsyncThunk( +export const approveLPToken = createAsyncThunk( 'earn/approveLPToken', async (_, { dispatch, extra: { sdk } }) => { - const hash = await sdk.token.approveLPToken(); + const { hash } = await sdk.kwentaToken.approveLPToken(); if (hash) { monitorTransaction({ @@ -19,14 +19,14 @@ export const approveLPToken = createAsyncThunk( } ); -export const stakeTokens = createAsyncThunk( +export const stakeTokens = createAsyncThunk( 'earn/stakeTokens', async (_, { dispatch, getState, extra: { sdk } }) => { const { earn: { amount }, } = getState(); - const hash = await sdk.token.changePoolTokens(amount, 'stake'); + const { hash } = await sdk.kwentaToken.changePoolTokens(amount, 'stake'); if (hash) { monitorTransaction({ @@ -39,14 +39,14 @@ export const stakeTokens = createAsyncThunk( } ); -export const unstakeTokens = createAsyncThunk( +export const unstakeTokens = createAsyncThunk( 'earn/unstakeTokens', async (_, { dispatch, getState, extra: { sdk } }) => { const { earn: { amount }, } = getState(); - const hash = await sdk.token.changePoolTokens(amount, 'withdraw'); + const { hash } = await sdk.kwentaToken.changePoolTokens(amount, 'withdraw'); if (hash) { monitorTransaction({ @@ -59,10 +59,10 @@ export const unstakeTokens = createAsyncThunk( } ); -export const claimRewards = createAsyncThunk( +export const claimRewards = createAsyncThunk( 'earn/claimRewards', async (_, { dispatch, extra: { sdk } }) => { - const hash = await sdk.token.claimRewards(); + const { hash } = await sdk.kwentaToken.claimRewards(); if (hash) { monitorTransaction({ @@ -86,7 +86,7 @@ export const getEarnDetails = createAsyncThunk('staking/fetchStakingData', async (_, { extra: { sdk } }) => { + const { + rewardEscrowBalance, + stakedNonEscrowedBalance, + stakedEscrowedBalance, + claimableBalance, + kwentaBalance, + weekCounter, + totalStakedBalance, + vKwentaBalance, + vKwentaAllowance, + kwentaAllowance, + epochPeriod, + veKwentaBalance, + veKwentaAllowance, + } = await sdk.kwentaToken.getStakingData(); + + return { + rewardEscrowBalance: rewardEscrowBalance.toString(), + stakedNonEscrowedBalance: stakedNonEscrowedBalance.toString(), + stakedEscrowedBalance: stakedEscrowedBalance.toString(), + claimableBalance: claimableBalance.toString(), + kwentaBalance: kwentaBalance.toString(), + weekCounter, + totalStakedBalance: totalStakedBalance.toString(), + vKwentaBalance: vKwentaBalance.toString(), + vKwentaAllowance: vKwentaAllowance.toString(), + kwentaAllowance: kwentaAllowance.toString(), + epochPeriod, + veKwentaBalance: veKwentaBalance.toString(), + veKwentaAllowance: veKwentaAllowance.toString(), + }; +}); + +export const approveKwentaToken = createAsyncThunk< + void, + 'kwenta' | 'vKwenta' | 'veKwenta', + ThunkConfig +>('staking/approveKwentaToken', async (token, { dispatch, extra: { sdk } }) => { + const { hash } = await sdk.kwentaToken.approveKwentaToken(token); + + monitorTransaction({ + txHash: hash, + onTxConfirmed: () => { + dispatch(fetchStakingData()); + }, + }); +}); + +export const redeemToken = createAsyncThunk( + 'staking/redeemToken', + async (token, { dispatch, extra: { sdk } }) => { + const { hash } = await sdk.kwentaToken.redeemToken( + token === 'vKwenta' ? 'vKwentaRedeemer' : 'veKwentaRedeemer', + { hasAddress: token === 'veKwenta' } + ); + + monitorTransaction({ + txHash: hash, + onTxConfirmed: () => { + dispatch(fetchStakingData()); + }, + }); + } +); + +export const fetchEscrowData = createAsyncThunk< + { escrowData: EscrowData[]; totalVestable: string }, + void, + ThunkConfig +>('staking/fetchEscrowData', async (_, { extra: { sdk } }) => { + const { escrowData, totalVestable } = await sdk.kwentaToken.getEscrowData(); + + return { + escrowData: escrowData.map((e) => ({ + ...e, + vestable: e.vestable.toString(), + amount: e.amount.toString(), + fee: e.fee.toString(), + })), + totalVestable: totalVestable.toString(), + }; +}); + +export const vestEscrowedRewards = createAsyncThunk( + 'staking/vestEscrowedRewards', + async (ids, { dispatch, extra: { sdk } }) => { + if (ids.length > 0) { + const { hash } = await sdk.kwentaToken.vestToken(ids); + + monitorTransaction({ + txHash: hash, + onTxConfirmed: () => { + dispatch({ type: 'staking/setVestEscrowedRewardsStatus', payload: FetchStatus.Success }); + dispatch(fetchStakingData()); + }, + onTxFailed: () => { + dispatch({ type: 'staking/setVestEscrowedRewardsStatus', payload: FetchStatus.Error }); + }, + }); + } + } +); + +export const getReward = createAsyncThunk( + 'staking/getReward', + async (_, { dispatch, extra: { sdk } }) => { + const { hash } = await sdk.kwentaToken.getReward(); + + monitorTransaction({ + txHash: hash, + onTxConfirmed: () => { + dispatch({ type: 'staking/setGetRewardStatus', payload: FetchStatus.Success }); + dispatch(fetchStakingData()); + }, + onTxFailed: () => { + dispatch({ type: 'staking/setGetRewardStatus', payload: FetchStatus.Error }); + }, + }); + } +); + +export const fetchClaimableRewards = createAsyncThunk< + { + claimableRewards: Awaited< + ReturnType + >['claimableRewards']; + totalRewards: string; + }, + void, + ThunkConfig +>('staking/fetchClaimableRewards', async (_, { getState, extra: { sdk } }) => { + const periods = selectPeriods(getState()); + + const { claimableRewards, totalRewards } = await sdk.kwentaToken.getClaimableRewards(periods); + + return { claimableRewards, totalRewards: totalRewards.toString() }; +}); + +export const claimMultipleRewards = createAsyncThunk( + 'staking/claimMultipleRewards', + async (_, { dispatch, getState, extra: { sdk } }) => { + const { + staking: { claimableRewards }, + } = getState(); + + const { hash } = await sdk.kwentaToken.claimMultipleRewards(claimableRewards); + + monitorTransaction({ + txHash: hash, + onTxConfirmed: () => { + dispatch({ type: 'staking/setClaimRewardsStatus', payload: FetchStatus.Success }); + dispatch(fetchStakingData()); + dispatch(fetchClaimableRewards()); + }, + onTxFailed: () => { + dispatch({ type: 'staking/setClaimRewardsStatus', payload: FetchStatus.Error }); + }, + }); + } +); + +export const stakeEscrow = createAsyncThunk( + 'staking/stakeEscrow', + async (amount, { dispatch, extra: { sdk } }) => { + const { hash } = await sdk.kwentaToken.stakeEscrowedKwenta(amount); + + monitorTransaction({ + txHash: hash, + onTxConfirmed: () => { + dispatch({ type: 'staking/setStakeEscrowedStatus', payload: FetchStatus.Success }); + dispatch(fetchStakingData()); + }, + onTxFailed: () => { + dispatch({ type: 'staking/setStakeEscrowedStatus', payload: FetchStatus.Error }); + }, + }); + } +); + +export const unstakeEscrow = createAsyncThunk( + 'staking/unstakeEscrow', + async (amount, { dispatch, extra: { sdk } }) => { + const { hash } = await sdk.kwentaToken.unstakeEscrowedKwenta(amount); + + monitorTransaction({ + txHash: hash, + onTxConfirmed: () => { + dispatch({ type: 'staking/setUnstakeEscrowedStatus', payload: FetchStatus.Success }); + dispatch(fetchStakingData()); + }, + onTxFailed: () => { + dispatch({ type: 'staking/setUnstakeEscrowedStatus', payload: FetchStatus.Error }); + }, + }); + } +); + +// TODO: Consider merging this with the (stake|unstake)Escrow actions. + +export const stakeKwenta = createAsyncThunk( + 'staking/stakeKwenta', + async (amount, { dispatch, extra: { sdk } }) => { + const { hash } = await sdk.kwentaToken.stakeKwenta(amount); + + monitorTransaction({ + txHash: hash, + onTxConfirmed: () => { + dispatch({ type: 'staking/setStakeStatus', payload: FetchStatus.Success }); + dispatch(fetchStakingData()); + }, + onTxFailed: () => { + dispatch({ type: 'staking/setStakeStatus', payload: FetchStatus.Error }); + }, + }); + } +); + +export const unstakeKwenta = createAsyncThunk( + 'staking/unstakeKwenta', + async (amount, { dispatch, extra: { sdk } }) => { + const { hash } = await sdk.kwentaToken.unstakeKwenta(amount); + + monitorTransaction({ + txHash: hash, + onTxConfirmed: () => { + dispatch({ type: 'staking/setUnstakeStatus', payload: FetchStatus.Success }); + dispatch(fetchStakingData()); + }, + onTxFailed: () => { + dispatch({ type: 'staking/setUnstakeStatus', payload: FetchStatus.Error }); + }, + }); + } +); diff --git a/state/staking/reducer.ts b/state/staking/reducer.ts new file mode 100644 index 0000000000..57b70f2580 --- /dev/null +++ b/state/staking/reducer.ts @@ -0,0 +1,124 @@ +import { createSlice } from '@reduxjs/toolkit'; + +import { FetchStatus } from 'state/types'; + +import { + claimMultipleRewards, + fetchClaimableRewards, + fetchEscrowData, + fetchStakingData, + getReward, + stakeEscrow, + stakeKwenta, + unstakeEscrow, + unstakeKwenta, + vestEscrowedRewards, +} from './actions'; +import { StakingState } from './types'; + +const initialState: StakingState = { + kwentaBalance: '0', + escrowedKwentaBalance: '0', + vKwentaBalance: '0', + veKwentaBalance: '0', + claimableBalance: '0', + totalStakedBalance: '0', + stakedEscrowedKwentaBalance: '0', + stakedKwentaBalance: '0', + epochPeriod: 0, + weekCounter: 1, + kwentaAllowance: '0', + vKwentaAllowance: '0', + veKwentaAllowance: '0', + totalVestable: '0', + escrowData: [], + totalRewards: '0', + claimableRewards: [], + stakeStatus: FetchStatus.Idle, + unstakeStatus: FetchStatus.Idle, + stakeEscrowedStatus: FetchStatus.Idle, + unstakeEscrowedStatus: FetchStatus.Idle, + getRewardStatus: FetchStatus.Idle, + claimRewardsStatus: FetchStatus.Idle, + vestEscrowedRewardsStatus: FetchStatus.Idle, +}; + +const stakingSlice = createSlice({ + name: 'staking', + initialState, + reducers: { + setStakeStatus: (state, action) => { + state.stakeStatus = action.payload; + }, + setUnstakeStatus: (state, action) => { + state.unstakeStatus = action.payload; + }, + setStakeEscrowedStatus: (state, action) => { + state.stakeEscrowedStatus = action.payload; + }, + setUnstakeEscrowedStatus: (state, action) => { + state.unstakeEscrowedStatus = action.payload; + }, + setGetRewardStatus: (state, action) => { + state.getRewardStatus = action.payload; + }, + setClaimRewardsStatus: (state, action) => { + state.claimRewardsStatus = action.payload; + }, + setVestEscrowedRewardsStatus: (state, action) => { + state.vestEscrowedRewardsStatus = action.payload; + }, + setSelectedEpoch: (state, action) => { + state.selectedEpoch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetchStakingData.fulfilled, (state, action) => { + state.escrowedKwentaBalance = action.payload.rewardEscrowBalance; + state.stakedKwentaBalance = action.payload.stakedNonEscrowedBalance; + state.stakedEscrowedKwentaBalance = action.payload.stakedEscrowedBalance; + state.claimableBalance = action.payload.claimableBalance; + state.kwentaBalance = action.payload.kwentaBalance; + state.weekCounter = action.payload.weekCounter; + state.totalStakedBalance = action.payload.totalStakedBalance; + state.vKwentaBalance = action.payload.vKwentaBalance; + state.vKwentaAllowance = action.payload.vKwentaAllowance; + state.kwentaAllowance = action.payload.kwentaAllowance; + state.epochPeriod = action.payload.epochPeriod; + state.veKwentaBalance = action.payload.veKwentaBalance; + state.veKwentaAllowance = action.payload.veKwentaAllowance; + }); + builder.addCase(fetchEscrowData.fulfilled, (state, action) => { + state.totalVestable = action.payload.totalVestable; + state.escrowData = action.payload.escrowData; + }); + builder.addCase(fetchClaimableRewards.fulfilled, (state, action) => { + state.claimableRewards = action.payload.claimableRewards; + state.totalRewards = action.payload.totalRewards; + }); + builder.addCase(stakeKwenta.pending, (state) => { + state.stakeStatus = FetchStatus.Loading; + }); + builder.addCase(unstakeKwenta.pending, (state) => { + state.unstakeStatus = FetchStatus.Loading; + }); + builder.addCase(stakeEscrow.pending, (state) => { + state.stakeEscrowedStatus = FetchStatus.Loading; + }); + builder.addCase(unstakeEscrow.pending, (state) => { + state.unstakeEscrowedStatus = FetchStatus.Loading; + }); + builder.addCase(getReward.pending, (state) => { + state.getRewardStatus = FetchStatus.Loading; + }); + builder.addCase(claimMultipleRewards.pending, (state) => { + state.claimRewardsStatus = FetchStatus.Loading; + }); + builder.addCase(vestEscrowedRewards.pending, (state) => { + state.vestEscrowedRewardsStatus = FetchStatus.Loading; + }); + }, +}); + +export default stakingSlice.reducer; +export const { setSelectedEpoch } = stakingSlice.actions; diff --git a/state/staking/selectors.ts b/state/staking/selectors.ts new file mode 100644 index 0000000000..cddeba5731 --- /dev/null +++ b/state/staking/selectors.ts @@ -0,0 +1,132 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { wei } from '@synthetixio/wei'; + +import { getEpochDetails, parseEpochData } from 'queries/staking/utils'; +import { RootState } from 'state/store'; +import { FetchStatus } from 'state/types'; +import { toWei } from 'utils/formatters/number'; + +export const selectKwentaBalance = createSelector( + (state: RootState) => state.staking.kwentaBalance, + toWei +); + +export const selectVKwentaBalance = createSelector( + (state: RootState) => state.staking.vKwentaBalance, + toWei +); + +export const selectVeKwentaBalance = createSelector( + (state: RootState) => state.staking.veKwentaBalance, + toWei +); + +export const selectEscrowedKwentaBalance = createSelector( + (state: RootState) => state.staking.escrowedKwentaBalance, + toWei +); + +export const selectStakedEscrowedKwentaBalance = createSelector( + (state: RootState) => state.staking.stakedEscrowedKwentaBalance, + toWei +); + +export const selectStakedKwentaBalance = createSelector( + (state: RootState) => state.staking.stakedKwentaBalance, + toWei +); + +export const selectClaimableBalance = createSelector( + (state: RootState) => state.staking.claimableBalance, + toWei +); + +export const selectPeriods = createSelector( + (state: RootState) => state.staking.epochPeriod, + (epochPeriod) => Array.from(new Array(epochPeriod + 1), (_, i) => i + 1) +); + +export const selectIsKwentaTokenApproved = createSelector( + selectKwentaBalance, + (state: RootState) => state.staking.kwentaAllowance, + (kwentaBalance, kwentaAllowance) => kwentaBalance.lte(kwentaAllowance) +); + +export const selectIsVKwentaTokenApproved = createSelector( + selectVKwentaBalance, + (state: RootState) => state.staking.vKwentaAllowance, + (vKwentaBalance, vKwentaAllowance) => vKwentaBalance.lte(vKwentaAllowance) +); + +export const selectIsVeKwentaTokenApproved = createSelector( + selectVeKwentaBalance, + (state: RootState) => state.staking.veKwentaAllowance, + (veKwentaBalance, veKwentaAllowance) => veKwentaBalance.lte(veKwentaAllowance) +); + +export const selectResetTime = createSelector( + (state: RootState) => state.wallet.networkId, + (state: RootState) => state.staking.epochPeriod, + (networkId, epochPeriod) => { + const { epochEnd } = getEpochDetails(networkId ?? 10, epochPeriod); + return epochEnd; + } +); + +export const selectEpochData = createSelector( + selectPeriods, + (state: RootState) => state.wallet.networkId, + (periods, networkId) => periods.map((i) => parseEpochData(i, networkId)) +); + +export const selectSelectedEpoch = createSelector( + (state: RootState) => state.staking.selectedEpoch, + (state: RootState) => state.staking.epochPeriod, + (state: RootState) => state.wallet.networkId, + (selectedEpoch, epochPeriod, networkId) => parseEpochData(selectedEpoch ?? epochPeriod, networkId) +); + +export const selectIsStakingKwenta = createSelector( + (state: RootState) => state.staking.stakeStatus, + (stakeStatus) => stakeStatus === FetchStatus.Loading +); + +export const selectIsUnstakingKwenta = createSelector( + (state: RootState) => state.staking.unstakeStatus, + (unstakeStatus) => unstakeStatus === FetchStatus.Loading +); + +export const selectIsStakingEscrowedKwenta = createSelector( + (state: RootState) => state.staking.stakeEscrowedStatus, + (stakeEscrowedStatus) => stakeEscrowedStatus === FetchStatus.Loading +); + +export const selectIsUnstakingEscrowedKwenta = createSelector( + (state: RootState) => state.staking.unstakeEscrowedStatus, + (unstakeEscrowedStatus) => unstakeEscrowedStatus === FetchStatus.Loading +); + +export const selectIsGettingReward = createSelector( + (state: RootState) => state.staking.getRewardStatus, + (getRewardStatus) => getRewardStatus === FetchStatus.Loading +); + +export const selectIsClaimingRewards = createSelector( + (state: RootState) => state.staking.claimRewardsStatus, + (claimRewardsStatus) => claimRewardsStatus === FetchStatus.Loading +); + +export const selectIsVestingEscrowedRewards = createSelector( + (state: RootState) => state.staking.vestEscrowedRewardsStatus, + (vestEscrowedRewardsStatus) => vestEscrowedRewardsStatus === FetchStatus.Loading +); + +export const selectTotalRewards = createSelector( + (state: RootState) => state.staking.totalRewards, + wei +); + +export const selectTotalVestable = createSelector( + (state: RootState) => state.staking.totalVestable, + wei +); diff --git a/state/staking/types.ts b/state/staking/types.ts new file mode 100644 index 0000000000..3a8ed1c49e --- /dev/null +++ b/state/staking/types.ts @@ -0,0 +1,31 @@ +import type { EscrowData } from 'sdk/services/kwentaToken'; +import { ClaimParams } from 'sdk/services/kwentaToken'; +import { FetchStatus } from 'state/types'; + +export type StakingState = { + kwentaBalance: string; + vKwentaBalance: string; + veKwentaBalance: string; + escrowedKwentaBalance: string; + claimableBalance: string; + totalStakedBalance: string; + stakedEscrowedKwentaBalance: string; + stakedKwentaBalance: string; + epochPeriod: number; + weekCounter: number; + kwentaAllowance: string; + vKwentaAllowance: string; + veKwentaAllowance: string; + totalVestable: string; + escrowData: EscrowData[]; + totalRewards: string; + claimableRewards: ClaimParams[]; + selectedEpoch?: number; + stakeStatus: FetchStatus; + unstakeStatus: FetchStatus; + stakeEscrowedStatus: FetchStatus; + unstakeEscrowedStatus: FetchStatus; + getRewardStatus: FetchStatus; + claimRewardsStatus: FetchStatus; + vestEscrowedRewardsStatus: FetchStatus; +}; diff --git a/state/store.ts b/state/store.ts index e5f42089b4..2f16b031fd 100644 --- a/state/store.ts +++ b/state/store.ts @@ -9,6 +9,7 @@ import earnReducer from './earn/reducer'; import exchangeReducer from './exchange/reducer'; import futuresReducer from './futures/reducer'; import homeReducer from './home/reducer'; +import stakingReducer from './staking/reducer'; import walletReducer from './wallet/reducer'; const LOG_REDUX = process.env.NODE_ENV !== 'production'; @@ -21,6 +22,7 @@ const store = configureStore({ futures: futuresReducer, home: homeReducer, earn: earnReducer, + staking: stakingReducer, }, middleware: (getDefaultMiddleware) => { const baseMiddleware = getDefaultMiddleware({ thunk: { extraArgument: { sdk } } }); From cb6fc9d7acf06f150b189c1770714567e00fcbe1 Mon Sep 17 00:00:00 2001 From: Oluwakorede Fashokun Date: Wed, 7 Dec 2022 16:15:49 -0700 Subject: [PATCH 05/24] fix claimable rewards (#1727) * Fix claimable rewards * Use correct period * Load after staking data is loaded * Delay until periods are set --- pages/dashboard/staking.tsx | 6 +++-- sdk/services/kwentaToken.ts | 27 ++++++++++++------- .../dashboard/Stake/TradingRewardsTab.tsx | 8 ++---- state/staking/selectors.ts | 2 +- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/pages/dashboard/staking.tsx b/pages/dashboard/staking.tsx index ce2f244a4b..19b2c45b70 100644 --- a/pages/dashboard/staking.tsx +++ b/pages/dashboard/staking.tsx @@ -6,7 +6,7 @@ import DashboardLayout from 'sections/dashboard/DashboardLayout'; import StakingPortfolio, { StakeTab } from 'sections/dashboard/Stake/StakingPortfolio'; import StakingTabs from 'sections/dashboard/Stake/StakingTabs'; import { useAppDispatch, useAppSelector } from 'state/hooks'; -import { fetchEscrowData, fetchStakingData } from 'state/staking/actions'; +import { fetchClaimableRewards, fetchEscrowData, fetchStakingData } from 'state/staking/actions'; type StakingComponent = React.FC & { getLayout: (page: HTMLElement) => JSX.Element }; @@ -18,7 +18,9 @@ const StakingPage: StakingComponent = () => { useEffect(() => { if (!!walletAddress) { - dispatch(fetchStakingData()); + dispatch(fetchStakingData()).then(() => { + dispatch(fetchClaimableRewards()); + }); dispatch(fetchEscrowData()); } }, [dispatch, walletAddress]); diff --git a/sdk/services/kwentaToken.ts b/sdk/services/kwentaToken.ts index 4844f2d4bd..6532158744 100644 --- a/sdk/services/kwentaToken.ts +++ b/sdk/services/kwentaToken.ts @@ -1,6 +1,7 @@ import Wei, { wei } from '@synthetixio/wei'; import axios from 'axios'; import { ethers, BigNumber } from 'ethers'; +import { formatEther } from 'ethers/lib/utils'; import moment from 'moment'; import KwentaSDK from 'sdk'; @@ -28,6 +29,7 @@ type EpochData = { proof: string[]; }; }; + period: number; }; export type EscrowData = { @@ -372,17 +374,22 @@ export default class KwentaTokenService { }epoch-${i}.json` ); - const responses: EpochData[] = []; - - for (const fileName of fileNames) { - const response = await client.get(fileName); - responses.push(response.data ?? null); - } + const responses: EpochData[] = await Promise.all( + fileNames.map(async (fileName, period) => { + const response = await client.get(fileName); + return { ...response.data, period }; + }) + ); const rewards = responses - .map((d, period) => { - const walletReward = d.claims[walletAddress]; - return [walletReward.index, walletAddress, walletReward.amount, walletReward.proof, period]; + .map((d) => { + const reward = d.claims[walletAddress]; + + if (reward) { + return [reward.index, walletAddress, reward.amount, reward.proof, d.period]; + } + + return null; }) .filter((x): x is ClaimParams => !!x); @@ -394,7 +401,7 @@ export default class KwentaTokenService { (acc, next, i) => { if (!claimed[i]) { acc.claimableRewards.push(next); - acc.totalRewards = acc.totalRewards.add(wei(next[2])); + acc.totalRewards = acc.totalRewards.add(wei(formatEther(next[2]))); } return acc; diff --git a/sections/dashboard/Stake/TradingRewardsTab.tsx b/sections/dashboard/Stake/TradingRewardsTab.tsx index 0246577a32..0472bd1dcf 100644 --- a/sections/dashboard/Stake/TradingRewardsTab.tsx +++ b/sections/dashboard/Stake/TradingRewardsTab.tsx @@ -1,6 +1,6 @@ import { wei } from '@synthetixio/wei'; import { formatEther } from 'ethers/lib/utils.js'; -import { useCallback, useEffect, useMemo, FC } from 'react'; +import { useCallback, useMemo, FC } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -16,7 +16,7 @@ import { TradingRewardProps, } from 'queries/staking/utils'; import { useAppDispatch, useAppSelector } from 'state/hooks'; -import { claimMultipleRewards, fetchClaimableRewards } from 'state/staking/actions'; +import { claimMultipleRewards } from 'state/staking/actions'; import { selectResetTime, selectTotalRewards } from 'state/staking/selectors'; import { FlexDivRow } from 'styles/common'; import media from 'styles/media'; @@ -57,10 +57,6 @@ const TradingRewardsTab: FC = ({ const claimDisabled = useMemo(() => totalRewards.lte(0), [totalRewards]); - useEffect(() => { - dispatch(fetchClaimableRewards()); - }, [dispatch]); - const handleClaim = useCallback(() => { dispatch(claimMultipleRewards()); }, [dispatch]); diff --git a/state/staking/selectors.ts b/state/staking/selectors.ts index cddeba5731..1596b4f3e0 100644 --- a/state/staking/selectors.ts +++ b/state/staking/selectors.ts @@ -43,7 +43,7 @@ export const selectClaimableBalance = createSelector( export const selectPeriods = createSelector( (state: RootState) => state.staking.epochPeriod, - (epochPeriod) => Array.from(new Array(epochPeriod + 1), (_, i) => i + 1) + (epochPeriod) => Array.from(new Array(epochPeriod + 1), (_, i) => i) ); export const selectIsKwentaTokenApproved = createSelector( From 584bc2c96dc96e9485b7530cbc7d75da0a768b77 Mon Sep 17 00:00:00 2001 From: leifu Date: Thu, 8 Dec 2022 01:56:42 +0200 Subject: [PATCH 06/24] close short position of DEBT (#1723) * Removed the gas fee check on close button * Applied the special rule to close the position of DebtRatio --- .../futures/PositionCard/ClosePositionModalCrossMargin.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sections/futures/PositionCard/ClosePositionModalCrossMargin.tsx b/sections/futures/PositionCard/ClosePositionModalCrossMargin.tsx index afe2d5cdfe..654769f980 100644 --- a/sections/futures/PositionCard/ClosePositionModalCrossMargin.tsx +++ b/sections/futures/PositionCard/ClosePositionModalCrossMargin.tsx @@ -44,7 +44,8 @@ export default function ClosePositionModalCrossMargin({ onDismiss }: Props) { const positionSize = useMemo(() => positionDetails?.size ?? zeroBN, [positionDetails?.size]); const crossMarginCloseParams = useMemo(() => { - return marketAsset === 'SOL' && position?.position?.side === PositionSide.SHORT + return ['SOL', 'DebtRatio'].includes(marketAsset) && + position?.position?.side === PositionSide.SHORT ? [ { marketKey: formatBytes32String(marketKey), From d2ce58d007165fa6be486277216869b79f59b2d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Dec 2022 15:42:56 -0300 Subject: [PATCH 07/24] Bump github/codeql-action from 2.1.35 to 2.1.36 (#1732) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.1.35 to 2.1.36. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b2a92eb56d8cb930006a1c6ed86b0782dd8a4297...a669cc5936cc5e1b6a362ec1ff9e410dc570d190) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/audit_build_verify.yml | 2 +- .github/workflows/codeql.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/audit_build_verify.yml b/.github/workflows/audit_build_verify.yml index 6a920fae2c..830bbf3d0d 100644 --- a/.github/workflows/audit_build_verify.yml +++ b/.github/workflows/audit_build_verify.yml @@ -60,7 +60,7 @@ jobs: run: npm run lint:sarif - name: Upload lint results - uses: github/codeql-action/upload-sarif@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # pin@codeql-bundle-20220322 + uses: github/codeql-action/upload-sarif@a669cc5936cc5e1b6a362ec1ff9e410dc570d190 # pin@codeql-bundle-20220322 with: sarif_file: lint-results.sarif continue-on-error: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7a058aebf2..7b64c42a2d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,13 +21,13 @@ jobs: uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v2 - name: Initialize CodeQL - uses: github/codeql-action/init@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # pin@codeql-bundle-20220322 + uses: github/codeql-action/init@a669cc5936cc5e1b6a362ec1ff9e410dc570d190 # pin@codeql-bundle-20220322 with: queries: security-and-quality languages: javascript - name: Autobuild - uses: github/codeql-action/autobuild@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # pin@codeql-bundle-20220322 + uses: github/codeql-action/autobuild@a669cc5936cc5e1b6a362ec1ff9e410dc570d190 # pin@codeql-bundle-20220322 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # pin@codeql-bundle-20220322 + uses: github/codeql-action/analyze@a669cc5936cc5e1b6a362ec1ff9e410dc570d190 # pin@codeql-bundle-20220322 From 55ce290e3a42a30893cbb7ef74745506d20dbbfe Mon Sep 17 00:00:00 2001 From: leifu Date: Sat, 10 Dec 2022 15:06:42 +0200 Subject: [PATCH 08/24] add estimated rewards (#1724) * Added estimated rewards by reading file * Corrected the epoch period * Added estimated rewards, description and fixed the css issue * Updated the explanation text to gray * Updated the format and text * Fixed the lint --- sections/dashboard/Stake/StakingTab.tsx | 8 +- .../dashboard/Stake/TradingRewardsTab.tsx | 78 +++++++++++++------ state/staking/selectors.ts | 5 ++ translations/en.json | 7 +- 4 files changed, 66 insertions(+), 32 deletions(-) diff --git a/sections/dashboard/Stake/StakingTab.tsx b/sections/dashboard/Stake/StakingTab.tsx index d53ae35b91..0cc20dfc50 100644 --- a/sections/dashboard/Stake/StakingTab.tsx +++ b/sections/dashboard/Stake/StakingTab.tsx @@ -64,13 +64,7 @@ const StakingTabContainer = styled.div` ${media.greaterThan('mdUp')` display: grid; grid-template-columns: 1fr 1fr; - & > div { - flex: 1; - - &:first-child { - margin-right: 15px; - } - } + grid-gap: 15px; `} ${media.lessThan('mdUp')` diff --git a/sections/dashboard/Stake/TradingRewardsTab.tsx b/sections/dashboard/Stake/TradingRewardsTab.tsx index 0472bd1dcf..11d5a3fa29 100644 --- a/sections/dashboard/Stake/TradingRewardsTab.tsx +++ b/sections/dashboard/Stake/TradingRewardsTab.tsx @@ -1,4 +1,5 @@ import { wei } from '@synthetixio/wei'; +import { BigNumber } from 'ethers'; import { formatEther } from 'ethers/lib/utils.js'; import { useCallback, useMemo, FC } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,6 +9,7 @@ import HelpIcon from 'assets/svg/app/question-mark.svg'; import Button from 'components/Button'; import StyledTooltip from 'components/Tooltip/StyledTooltip'; import Connector from 'containers/Connector'; +import useGetFile from 'queries/files/useGetFile'; import useGetFuturesFee from 'queries/staking/useGetFuturesFee'; import useGetFuturesFeeForAccount from 'queries/staking/useGetFuturesFeeForAccount'; import { @@ -17,7 +19,7 @@ import { } from 'queries/staking/utils'; import { useAppDispatch, useAppSelector } from 'state/hooks'; import { claimMultipleRewards } from 'state/staking/actions'; -import { selectResetTime, selectTotalRewards } from 'state/staking/selectors'; +import { selectEpochPeriod, selectResetTime, selectTotalRewards } from 'state/staking/selectors'; import { FlexDivRow } from 'styles/common'; import media from 'styles/media'; import { formatTruncatedDuration } from 'utils/formatters/date'; @@ -31,11 +33,12 @@ const TradingRewardsTab: FC = ({ end = Math.floor(Date.now() / 1000), }) => { const { t } = useTranslation(); - const { walletAddress } = Connector.useContainer(); + const { walletAddress, network } = Connector.useContainer(); const dispatch = useAppDispatch(); const resetTime = useAppSelector(selectResetTime); const totalRewards = useAppSelector(selectTotalRewards); + const epochPeriod = useAppSelector(selectEpochPeriod); const futuresFeeQuery = useGetFuturesFeeForAccount(walletAddress!, start, end); const futuresFeePaid = useMemo(() => { @@ -55,17 +58,28 @@ const TradingRewardsTab: FC = ({ .reduce((acc, curr) => acc.add(wei(curr)), zeroBN); }, [totalFuturesFeeQuery.data]); + const estimatedRewardQuery = useGetFile( + `trading-rewards-snapshots/${network.id === 420 ? `goerli-` : ''}epoch-current.json` + ); + const estimatedReward = useMemo( + () => BigNumber.from(estimatedRewardQuery?.data?.claims[walletAddress!]?.amount ?? 0), + [estimatedRewardQuery?.data?.claims, walletAddress] + ); + const weeklyRewards = useMemo(() => BigNumber.from(estimatedRewardQuery?.data?.tokenTotal ?? 0), [ + estimatedRewardQuery?.data?.tokenTotal, + ]); + const claimDisabled = useMemo(() => totalRewards.lte(0), [totalRewards]); const handleClaim = useCallback(() => { dispatch(claimMultipleRewards()); }, [dispatch]); - const ratio = useMemo( - () => - wei(totalFuturesFeePaid).gt(0) ? wei(futuresFeePaid).div(wei(totalFuturesFeePaid)) : zeroBN, - [futuresFeePaid, totalFuturesFeePaid] - ); + const ratio = useMemo(() => { + return wei(weeklyRewards).gt(0) ? wei(estimatedReward).div(wei(weeklyRewards)) : zeroBN; + }, [estimatedReward, weeklyRewards]); + + const showEstimatedValue = useMemo(() => wei(period).eq(epochPeriod), [epochPeriod, period]); return ( @@ -88,11 +102,11 @@ const TradingRewardsTab: FC = ({
- + - + @@ -122,20 +136,44 @@ const TradingRewardsTab: FC = ({
{formatDollars(totalFuturesFeePaid, { minDecimals: 2 })}
-
-
- {t('dashboard.stake.tabs.trading-rewards.estimated-fee-share', { - EpochPeriod: period, - })} -
-
{formatPercent(ratio, { minDecimals: 2 })}
-
+ {showEstimatedValue ? ( + <> +
+
+ {t('dashboard.stake.tabs.trading-rewards.estimated-rewards')} +
+ {truncateNumbers(wei(estimatedReward), 4)} +
+
+
+ {t('dashboard.stake.tabs.trading-rewards.estimated-reward-share', { + EpochPeriod: period, + })} +
+
{formatPercent(ratio, { minDecimals: 2 })}
+
+ + ) : null}
+ {showEstimatedValue ? ( + + {t('dashboard.stake.tabs.trading-rewards.estimated-info')} + + ) : null}
); }; +const PeriodLabel = styled.div` + font-size: 13px; + line-height: 20px; + display: flex; + align-items: center; + font-family: ${(props) => props.theme.fonts.regular}; + color: ${(props) => props.theme.colors.selectedTheme.gray}; +`; + const CustomStyledTooltip = styled(StyledTooltip)` padding: 0px 10px 0px; ${media.lessThan('md')` @@ -148,10 +186,6 @@ const WithCursor = styled.div<{ cursor: 'help' }>` cursor: ${(props) => props.cursor}; `; -const StyledFlexDivRow = styled(FlexDivRow)` - column-gap: 15px; -`; - const CardGridContainer = styled(StakingCard)` display: flex; flex-direction: column; @@ -172,7 +206,7 @@ const CardGrid = styled.div` } svg { - margin-left: 5px; + margin-left: 8px; } .title { diff --git a/state/staking/selectors.ts b/state/staking/selectors.ts index 1596b4f3e0..cd8ac93574 100644 --- a/state/staking/selectors.ts +++ b/state/staking/selectors.ts @@ -130,3 +130,8 @@ export const selectTotalVestable = createSelector( (state: RootState) => state.staking.totalVestable, wei ); + +export const selectEpochPeriod = createSelector( + (state: RootState) => state.staking.epochPeriod, + wei +); diff --git a/translations/en.json b/translations/en.json index 4d98291f07..341615d9a8 100644 --- a/translations/en.json +++ b/translations/en.json @@ -524,14 +524,15 @@ "spot-fee-paid": "Spot Fee Paid: Epoch {{EpochPeriod}}", "future-fee-paid": "Futures Fees Paid: Ep. {{EpochPeriod}}", "fees-paid": "Total Fees in Pool: Ep. {{EpochPeriod}}", - "estimated-rewards": "Estimated Rewards", - "estimated-fee-share": "Estimated Fee Share", + "estimated-rewards": "Estimated Reward*", + "estimated-reward-share": "Estimated Reward Share*", "trading-activity-reset": "Time until next epoch", "epoch": "Epoch {{EpochPeriod}}: {{EpochDate}}", "claimable-rewards-epoch": "Claimable Rewards: Epoch {{EpochPeriod}}", "claimable-rewards-all": "Claimable Trading Rewards", "claim-epoch": "Claim: Epoch {{EpochPeriod}}", - "claim": "Claim" + "claim": "Claim", + "estimated-info": "* Estimated values do not reflect the final reward value and are subject to change as a result of Futures fees paid and staked amounts by other participants." }, "escrow": { "title": "Escrow", From 7d8186478465d571a11b16954697668a48869d07 Mon Sep 17 00:00:00 2001 From: leifu Date: Mon, 12 Dec 2022 14:40:38 +0200 Subject: [PATCH 09/24] add atomic price feed (#1722) * Added atomic price feed * Fixed the issue that susd is base currency * Fixed the issue for non-susd atomic swap * Removed the log * Removed unecessary changes * Fixed the exchange fee rate * Fixed the lint * Update the both rate with atomic swap feed --- sdk/contracts/abis/ExchangeRates.json | 252 ----------------- sdk/contracts/types/ExchangeRates.ts | 92 +------ .../types/factories/ExchangeRates__factory.ts | 255 ------------------ sdk/services/exchange.ts | 41 ++- 4 files changed, 38 insertions(+), 602 deletions(-) diff --git a/sdk/contracts/abis/ExchangeRates.json b/sdk/contracts/abis/ExchangeRates.json index c6522f471e..4473e67f10 100644 --- a/sdk/contracts/abis/ExchangeRates.json +++ b/sdk/contracts/abis/ExchangeRates.json @@ -341,258 +341,6 @@ "stateMutability": "view", "type": "function" }, - { - "constant": true, - "inputs": [ - { - "components": [ - { - "internalType": "bytes32", - "name": "currencyKey", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "dexPriceAggregator", - "type": "address" - }, - { - "internalType": "address", - "name": "atomicEquivalentForDexPricing", - "type": "address" - }, - { - "internalType": "uint256", - "name": "atomicExchangeFeeRate", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "atomicTwapWindow", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "atomicMaxVolumePerBlock", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "atomicVolatilityConsiderationWindow", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "atomicVolatilityUpdateThreshold", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeFeeRate", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeMaxDynamicFee", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeDynamicFeeRounds", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeDynamicFeeThreshold", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeDynamicFeeWeightDecay", - "type": "uint256" - } - ], - "internalType": "struct IDirectIntegrationManager.ParameterIntegrationSettings", - "name": "", - "type": "tuple" - }, - { - "internalType": "uint256", - "name": "", - "type": "uint256" - }, - { - "components": [ - { - "internalType": "bytes32", - "name": "currencyKey", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "dexPriceAggregator", - "type": "address" - }, - { - "internalType": "address", - "name": "atomicEquivalentForDexPricing", - "type": "address" - }, - { - "internalType": "uint256", - "name": "atomicExchangeFeeRate", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "atomicTwapWindow", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "atomicMaxVolumePerBlock", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "atomicVolatilityConsiderationWindow", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "atomicVolatilityUpdateThreshold", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeFeeRate", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeMaxDynamicFee", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeDynamicFeeRounds", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeDynamicFeeThreshold", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeDynamicFeeWeightDecay", - "type": "uint256" - } - ], - "internalType": "struct IDirectIntegrationManager.ParameterIntegrationSettings", - "name": "", - "type": "tuple" - }, - { - "components": [ - { - "internalType": "bytes32", - "name": "currencyKey", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "dexPriceAggregator", - "type": "address" - }, - { - "internalType": "address", - "name": "atomicEquivalentForDexPricing", - "type": "address" - }, - { - "internalType": "uint256", - "name": "atomicExchangeFeeRate", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "atomicTwapWindow", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "atomicMaxVolumePerBlock", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "atomicVolatilityConsiderationWindow", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "atomicVolatilityUpdateThreshold", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeFeeRate", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeMaxDynamicFee", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeDynamicFeeRounds", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeDynamicFeeThreshold", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "exchangeDynamicFeeWeightDecay", - "type": "uint256" - } - ], - "internalType": "struct IDirectIntegrationManager.ParameterIntegrationSettings", - "name": "", - "type": "tuple" - } - ], - "name": "effectiveAtomicValueAndRates", - "outputs": [ - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "systemValue", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "systemSourceRate", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "systemDestinationRate", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, { "constant": true, "inputs": [ diff --git a/sdk/contracts/types/ExchangeRates.ts b/sdk/contracts/types/ExchangeRates.ts index fd777924df..fc9822723d 100644 --- a/sdk/contracts/types/ExchangeRates.ts +++ b/sdk/contracts/types/ExchangeRates.ts @@ -88,7 +88,6 @@ export interface ExchangeRatesInterface extends utils.Interface { "currenciesUsingAggregator(address)": FunctionFragment; "currencyKeyDecimals(bytes32)": FunctionFragment; "effectiveAtomicValueAndRates(bytes32,uint256,bytes32)": FunctionFragment; - "effectiveAtomicValueAndRates((bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),uint256,(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))": FunctionFragment; "effectiveValue(bytes32,uint256,bytes32)": FunctionFragment; "effectiveValueAndRates(bytes32,uint256,bytes32)": FunctionFragment; "effectiveValueAndRatesAtRound(bytes32,uint256,bytes32,uint256,uint256)": FunctionFragment; @@ -132,8 +131,7 @@ export interface ExchangeRatesInterface extends utils.Interface { | "anyRateIsInvalidAtRound" | "currenciesUsingAggregator" | "currencyKeyDecimals" - | "effectiveAtomicValueAndRates(bytes32,uint256,bytes32)" - | "effectiveAtomicValueAndRates((bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),uint256,(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))" + | "effectiveAtomicValueAndRates" | "effectiveValue" | "effectiveValueAndRates" | "effectiveValueAndRatesAtRound" @@ -206,22 +204,13 @@ export interface ExchangeRatesInterface extends utils.Interface { values: [PromiseOrValue] ): string; encodeFunctionData( - functionFragment: "effectiveAtomicValueAndRates(bytes32,uint256,bytes32)", + functionFragment: "effectiveAtomicValueAndRates", values: [ PromiseOrValue, PromiseOrValue, PromiseOrValue ] ): string; - encodeFunctionData( - functionFragment: "effectiveAtomicValueAndRates((bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),uint256,(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))", - values: [ - IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - PromiseOrValue, - IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - IDirectIntegrationManager.ParameterIntegrationSettingsStruct - ] - ): string; encodeFunctionData( functionFragment: "effectiveValue", values: [ @@ -397,11 +386,7 @@ export interface ExchangeRatesInterface extends utils.Interface { data: BytesLike ): Result; decodeFunctionResult( - functionFragment: "effectiveAtomicValueAndRates(bytes32,uint256,bytes32)", - data: BytesLike - ): Result; - decodeFunctionResult( - functionFragment: "effectiveAtomicValueAndRates((bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),uint256,(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))", + functionFragment: "effectiveAtomicValueAndRates", data: BytesLike ): Result; decodeFunctionResult( @@ -657,7 +642,7 @@ export interface ExchangeRates extends BaseContract { overrides?: CallOverrides ): Promise<[number]>; - "effectiveAtomicValueAndRates(bytes32,uint256,bytes32)"( + effectiveAtomicValueAndRates( arg0: PromiseOrValue, arg1: PromiseOrValue, arg2: PromiseOrValue, @@ -671,21 +656,6 @@ export interface ExchangeRates extends BaseContract { } >; - "effectiveAtomicValueAndRates((bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),uint256,(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))"( - arg0: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - arg1: PromiseOrValue, - arg2: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - arg3: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - overrides?: CallOverrides - ): Promise< - [BigNumber, BigNumber, BigNumber, BigNumber] & { - value: BigNumber; - systemValue: BigNumber; - systemSourceRate: BigNumber; - systemDestinationRate: BigNumber; - } - >; - effectiveValue( sourceCurrencyKey: PromiseOrValue, sourceAmount: PromiseOrValue, @@ -890,7 +860,7 @@ export interface ExchangeRates extends BaseContract { overrides?: CallOverrides ): Promise; - "effectiveAtomicValueAndRates(bytes32,uint256,bytes32)"( + effectiveAtomicValueAndRates( arg0: PromiseOrValue, arg1: PromiseOrValue, arg2: PromiseOrValue, @@ -904,21 +874,6 @@ export interface ExchangeRates extends BaseContract { } >; - "effectiveAtomicValueAndRates((bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),uint256,(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))"( - arg0: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - arg1: PromiseOrValue, - arg2: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - arg3: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - overrides?: CallOverrides - ): Promise< - [BigNumber, BigNumber, BigNumber, BigNumber] & { - value: BigNumber; - systemValue: BigNumber; - systemSourceRate: BigNumber; - systemDestinationRate: BigNumber; - } - >; - effectiveValue( sourceCurrencyKey: PromiseOrValue, sourceAmount: PromiseOrValue, @@ -1119,7 +1074,7 @@ export interface ExchangeRates extends BaseContract { overrides?: CallOverrides ): Promise; - "effectiveAtomicValueAndRates(bytes32,uint256,bytes32)"( + effectiveAtomicValueAndRates( arg0: PromiseOrValue, arg1: PromiseOrValue, arg2: PromiseOrValue, @@ -1133,21 +1088,6 @@ export interface ExchangeRates extends BaseContract { } >; - "effectiveAtomicValueAndRates((bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),uint256,(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))"( - arg0: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - arg1: PromiseOrValue, - arg2: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - arg3: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - overrides?: CallOverrides - ): Promise< - [BigNumber, BigNumber, BigNumber, BigNumber] & { - value: BigNumber; - systemValue: BigNumber; - systemSourceRate: BigNumber; - systemDestinationRate: BigNumber; - } - >; - effectiveValue( sourceCurrencyKey: PromiseOrValue, sourceAmount: PromiseOrValue, @@ -1390,21 +1330,13 @@ export interface ExchangeRates extends BaseContract { overrides?: CallOverrides ): Promise; - "effectiveAtomicValueAndRates(bytes32,uint256,bytes32)"( + effectiveAtomicValueAndRates( arg0: PromiseOrValue, arg1: PromiseOrValue, arg2: PromiseOrValue, overrides?: CallOverrides ): Promise; - "effectiveAtomicValueAndRates((bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),uint256,(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))"( - arg0: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - arg1: PromiseOrValue, - arg2: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - arg3: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - overrides?: CallOverrides - ): Promise; - effectiveValue( sourceCurrencyKey: PromiseOrValue, sourceAmount: PromiseOrValue, @@ -1594,21 +1526,13 @@ export interface ExchangeRates extends BaseContract { overrides?: CallOverrides ): Promise; - "effectiveAtomicValueAndRates(bytes32,uint256,bytes32)"( + effectiveAtomicValueAndRates( arg0: PromiseOrValue, arg1: PromiseOrValue, arg2: PromiseOrValue, overrides?: CallOverrides ): Promise; - "effectiveAtomicValueAndRates((bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),uint256,(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256),(bytes32,address,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))"( - arg0: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - arg1: PromiseOrValue, - arg2: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - arg3: IDirectIntegrationManager.ParameterIntegrationSettingsStruct, - overrides?: CallOverrides - ): Promise; - effectiveValue( sourceCurrencyKey: PromiseOrValue, sourceAmount: PromiseOrValue, diff --git a/sdk/contracts/types/factories/ExchangeRates__factory.ts b/sdk/contracts/types/factories/ExchangeRates__factory.ts index 61e8474d45..9814fa44f7 100644 --- a/sdk/contracts/types/factories/ExchangeRates__factory.ts +++ b/sdk/contracts/types/factories/ExchangeRates__factory.ts @@ -349,261 +349,6 @@ const _abi = [ stateMutability: "view", type: "function", }, - { - constant: true, - inputs: [ - { - components: [ - { - internalType: "bytes32", - name: "currencyKey", - type: "bytes32", - }, - { - internalType: "address", - name: "dexPriceAggregator", - type: "address", - }, - { - internalType: "address", - name: "atomicEquivalentForDexPricing", - type: "address", - }, - { - internalType: "uint256", - name: "atomicExchangeFeeRate", - type: "uint256", - }, - { - internalType: "uint256", - name: "atomicTwapWindow", - type: "uint256", - }, - { - internalType: "uint256", - name: "atomicMaxVolumePerBlock", - type: "uint256", - }, - { - internalType: "uint256", - name: "atomicVolatilityConsiderationWindow", - type: "uint256", - }, - { - internalType: "uint256", - name: "atomicVolatilityUpdateThreshold", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeFeeRate", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeMaxDynamicFee", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeDynamicFeeRounds", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeDynamicFeeThreshold", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeDynamicFeeWeightDecay", - type: "uint256", - }, - ], - internalType: - "struct IDirectIntegrationManager.ParameterIntegrationSettings", - name: "", - type: "tuple", - }, - { - internalType: "uint256", - name: "", - type: "uint256", - }, - { - components: [ - { - internalType: "bytes32", - name: "currencyKey", - type: "bytes32", - }, - { - internalType: "address", - name: "dexPriceAggregator", - type: "address", - }, - { - internalType: "address", - name: "atomicEquivalentForDexPricing", - type: "address", - }, - { - internalType: "uint256", - name: "atomicExchangeFeeRate", - type: "uint256", - }, - { - internalType: "uint256", - name: "atomicTwapWindow", - type: "uint256", - }, - { - internalType: "uint256", - name: "atomicMaxVolumePerBlock", - type: "uint256", - }, - { - internalType: "uint256", - name: "atomicVolatilityConsiderationWindow", - type: "uint256", - }, - { - internalType: "uint256", - name: "atomicVolatilityUpdateThreshold", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeFeeRate", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeMaxDynamicFee", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeDynamicFeeRounds", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeDynamicFeeThreshold", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeDynamicFeeWeightDecay", - type: "uint256", - }, - ], - internalType: - "struct IDirectIntegrationManager.ParameterIntegrationSettings", - name: "", - type: "tuple", - }, - { - components: [ - { - internalType: "bytes32", - name: "currencyKey", - type: "bytes32", - }, - { - internalType: "address", - name: "dexPriceAggregator", - type: "address", - }, - { - internalType: "address", - name: "atomicEquivalentForDexPricing", - type: "address", - }, - { - internalType: "uint256", - name: "atomicExchangeFeeRate", - type: "uint256", - }, - { - internalType: "uint256", - name: "atomicTwapWindow", - type: "uint256", - }, - { - internalType: "uint256", - name: "atomicMaxVolumePerBlock", - type: "uint256", - }, - { - internalType: "uint256", - name: "atomicVolatilityConsiderationWindow", - type: "uint256", - }, - { - internalType: "uint256", - name: "atomicVolatilityUpdateThreshold", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeFeeRate", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeMaxDynamicFee", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeDynamicFeeRounds", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeDynamicFeeThreshold", - type: "uint256", - }, - { - internalType: "uint256", - name: "exchangeDynamicFeeWeightDecay", - type: "uint256", - }, - ], - internalType: - "struct IDirectIntegrationManager.ParameterIntegrationSettings", - name: "", - type: "tuple", - }, - ], - name: "effectiveAtomicValueAndRates", - outputs: [ - { - internalType: "uint256", - name: "value", - type: "uint256", - }, - { - internalType: "uint256", - name: "systemValue", - type: "uint256", - }, - { - internalType: "uint256", - name: "systemSourceRate", - type: "uint256", - }, - { - internalType: "uint256", - name: "systemDestinationRate", - type: "uint256", - }, - ], - payable: false, - stateMutability: "view", - type: "function", - }, { constant: true, inputs: [ diff --git a/sdk/services/exchange.ts b/sdk/services/exchange.ts index ec2d10161f..f330c9afb8 100644 --- a/sdk/services/exchange.ts +++ b/sdk/services/exchange.ts @@ -18,7 +18,6 @@ import { } from 'constants/currency'; import { DEFAULT_1INCH_SLIPPAGE } from 'constants/defaults'; import { ATOMIC_EXCHANGE_SLIPPAGE } from 'constants/exchange'; -import { ETH_UNIT } from 'constants/network'; import { CG_BASE_API_URL } from 'queries/coingecko/constants'; import { PriceResponse } from 'queries/coingecko/types'; import { KWENTA_TRACKING_CODE } from 'queries/futures/constants'; @@ -33,7 +32,7 @@ import { newGetExchangeRatesForCurrencies, newGetExchangeRatesTupleForCurrencies, } from 'utils/currencies'; -import { zeroBN } from 'utils/formatters/number'; +import { UNIT_BIG_NUM, zeroBN } from 'utils/formatters/number'; import { FuturesMarketKey, MarketAssetByKey } from 'utils/futures'; import { getTransactionPrice, normalizeGasLimit } from 'utils/network'; @@ -168,7 +167,7 @@ export default class ExchangeService { ]); return sourceCurrencyFeeRate && destinationCurrencyFeeRate - ? wei(sourceCurrencyFeeRate.add(destinationCurrencyFeeRate)).div(ETH_UNIT) + ? wei(sourceCurrencyFeeRate.add(destinationCurrencyFeeRate)) : wei(0); } @@ -436,6 +435,20 @@ export default class ExchangeService { return exchangeRates; } + public async getAtomicRates(currencyKey: string) { + if (!this.sdk.context.contracts.ExchangeRates) { + throw new Error(sdkErrors.UNSUPPORTED_NETWORK); + } + + const { value } = await this.sdk.context.contracts.ExchangeRates.effectiveAtomicValueAndRates( + ethers.utils.formatBytes32String(currencyKey), + UNIT_BIG_NUM, + ethers.utils.formatBytes32String('sUSD') + ); + + return wei(value) ?? wei(0); + } + public async approveSwap(quoteCurrencyKey: string, baseCurrencyKey: string) { const txProvider = this.getTxProvider(baseCurrencyKey, quoteCurrencyKey); const [oneInchApproveAddress, quoteCurrencyContract] = await Promise.all([ @@ -524,6 +537,7 @@ export default class ExchangeService { ); } else { const isAtomic = this.checkIsAtomic(baseCurrencyKey, quoteCurrencyKey); + const exchangeParams = this.getExchangeParams( quoteCurrencyKey, baseCurrencyKey, @@ -729,7 +743,9 @@ export default class ExchangeService { return wei(0); } } else { - return newGetExchangeRatesForCurrencies(this.exchangeRates, currencyKey, 'sUSD'); + return this.checkIsAtomic(currencyKey, 'sUSD') + ? await this.getAtomicRates(currencyKey) + : newGetExchangeRatesForCurrencies(this.exchangeRates, currencyKey, 'sUSD'); } } @@ -969,13 +985,16 @@ export default class ExchangeService { } private async getPairRates(quoteCurrencyKey: string, baseCurrencyKey: string) { - const pairRates = newGetExchangeRatesTupleForCurrencies( - this.exchangeRates, - quoteCurrencyKey, - baseCurrencyKey - ); - - return pairRates; + return this.checkIsAtomic(baseCurrencyKey, quoteCurrencyKey) + ? await Promise.all([ + this.getAtomicRates(quoteCurrencyKey), + this.getAtomicRates(baseCurrencyKey), + ]) + : newGetExchangeRatesTupleForCurrencies( + this.exchangeRates, + quoteCurrencyKey, + baseCurrencyKey + ); } private async getOneInchApproveAddress() { From f17b59215b681c4e38df3dcd3be3b74abb620460 Mon Sep 17 00:00:00 2001 From: Adam Clarke Date: Mon, 12 Dec 2022 20:32:04 +0000 Subject: [PATCH 10/24] Futures refactor (#1702) * Futures refactor continued * Refactor isolated margin transfer actions * Refactor balances * Refactor futures data polling * Refactor orders and cross margin settings queries * Multicall contract updates * get positions by account type * fetch all positions when provider ready * fix initial position fetch * Simplify polling pattern * use market keys to index (#1712) use market keys to index * Refactor isolated margin actions * Move close isolated margin position to sdk and redux * Refactor trade previews * Fix cross margin leverage * Improve cross margin preview speed * Refactor trade inputs * Fix shorts and dashboard orders count * Futures refactor continued * fix trade preview on position side change * Clean up * Update state/balances/selectors.ts Co-authored-by: troyb.eth Co-authored-by: Troy Co-authored-by: troyb.eth --- components/Loader/Loader.tsx | 4 + components/TVChart/TVChart.tsx | 4 +- constants/queryKeys.ts | 40 - containers/Connector/Connector.tsx | 2 +- contexts/RefetchContext.tsx | 124 +-- hooks/useAverageEntryPrice.ts | 6 +- hooks/useFuturesData.ts | 467 ++-------- hooks/useMonitorTransactions.ts | 27 + pages/_app.tsx | 2 + pages/dashboard/index.tsx | 5 +- pages/dashboard/markets.tsx | 4 +- pages/market.tsx | 14 +- queries/futures/types.ts | 102 +-- .../useGetAverageFundingRateForMarket.ts | 102 --- .../useGetAverageFundingRateForMarkets.ts | 174 ---- .../useGetCrossMarginAccountOverview.ts | 81 -- queries/futures/useGetCrossMarginSettings.ts | 50 -- .../futures/useGetFuturesMarginTransfers.ts | 8 +- queries/futures/useGetFuturesMarkets.ts | 151 ---- queries/futures/useGetFuturesOpenOrders.ts | 74 -- .../futures/useGetFuturesPositionForMarket.ts | 70 -- .../useGetFuturesPositionForMarkets.ts | 131 --- .../useGetFuturesPotentialTradeDetails.ts | 165 ---- .../futures/useGetFuturesTradesForAccount.ts | 6 +- queries/futures/useGetFuturesVolumes.ts | 67 -- queries/futures/useQueryCrossMarginAccount.ts | 10 +- queries/futures/utils.ts | 77 +- queries/synths/useSynthBalances.ts | 79 -- queries/walletBalances/types.ts | 11 - sdk/README.md | 15 +- sdk/constants/futures.ts | 2 + .../contracts/FuturesMarketInternal.ts | 81 +- sdk/contracts/constants.ts | 5 + sdk/contracts/index.ts | 3 + sdk/services/exchange.ts | 5 +- sdk/services/futures.ts | 334 ++++++- sdk/services/synths.ts | 4 +- sdk/types/common.ts | 7 + sdk/types/futures.ts | 130 ++- sdk/types/tokens.ts | 28 + sdk/utils/futures.ts | 186 +++- .../FuturesHistoryTable.tsx | 6 +- .../FuturesMarketsTable.tsx | 22 +- .../FuturesPositionsTable.tsx | 18 +- .../MobileDashboard/FuturesMarkets.tsx | 6 +- .../MobileDashboard/OpenPositions.tsx | 24 +- sections/dashboard/Overview/Overview.tsx | 26 +- .../PortfolioChart/PortfolioChart.tsx | 9 +- .../SynthBalancesTable/SynthBalancesTable.tsx | 9 +- .../CrossMarginOnboard/CrossMarginOnboard.tsx | 8 +- sections/futures/FeeInfoBox/FeeInfoBox.tsx | 77 +- .../futures/LeverageInput/LeverageInput.tsx | 62 +- .../futures/MarketDetails/useGetMarketData.ts | 5 +- .../futures/MarketInfoBox/MarketInfoBox.tsx | 113 +-- .../MobileTrade/OverviewTabs/AccountTab.tsx | 6 +- .../futures/MobileTrade/PositionDetails.tsx | 6 +- .../futures/MobileTrade/UserTabs/UserTabs.tsx | 6 +- .../MobileTrade/drawers/OrderDrawer.tsx | 3 +- .../drawers/TradeConfirmationDrawer.tsx | 119 --- .../OrderPriceInput/OrderPriceInput.tsx | 12 +- .../futures/OrderSizing/OrderSizeSlider.tsx | 52 +- sections/futures/OrderSizing/OrderSizing.tsx | 106 +-- .../PositionCard/ClosePositionModal.tsx | 24 +- .../ClosePositionModalCrossMargin.tsx | 19 +- .../ClosePositionModalIsolatedMargin.tsx | 65 +- .../futures/PositionCard/PositionCard.tsx | 34 +- .../futures/PositionChart/PositionChart.tsx | 27 +- .../futures/ShareModal/AmountContainer.tsx | 2 +- .../futures/ShareModal/PositionMetadata.tsx | 6 +- sections/futures/ShareModal/ShareModal.tsx | 2 +- sections/futures/Trade/ManagePosition.tsx | 136 ++- sections/futures/Trade/MarketActions.tsx | 30 +- sections/futures/Trade/MarketsDropdown.tsx | 7 +- .../Trade/NextPriceConfirmationModal.tsx | 87 +- .../futures/Trade/TradeConfirmationModal.tsx | 46 +- .../TradeConfirmationModalCrossMargin.tsx | 47 +- .../TradeConfirmationModalIsolatedMargin.tsx | 51 +- .../futures/Trade/TradeIsolatedMargin.tsx | 39 +- .../Trade/TransferIsolatedMarginModal.tsx | 105 +-- .../TradeCrossMargin/CrossMarginInfoBox.tsx | 117 ++- .../DepositWithdrawCrossMargin.tsx | 120 +-- ...l.tsx => EditCrossMarginLeverageModal.tsx} | 85 +- .../ManageKeeperBalanceModal.tsx | 14 +- .../TradeCrossMargin/TradeCrossMargin.tsx | 67 +- sections/futures/UserInfo/OpenOrdersTable.tsx | 10 +- sections/futures/UserInfo/UserInfo.tsx | 13 +- sections/futures/types.ts | 31 - sections/homepage/Assets/Assets.tsx | 7 +- .../AppLayout/Header/BalanceActions.tsx | 36 +- .../SelectCurrencyModal.tsx | 32 +- state/app/helpers.ts | 17 + state/app/hooks.ts | 7 +- state/app/reducer.ts | 31 + state/app/selectors.ts | 9 + state/app/types.ts | 25 + state/balances/actions.ts | 46 +- state/balances/reducer.ts | 37 +- state/balances/selectors.ts | 17 +- state/balances/types.ts | 23 +- state/exchange/actions.ts | 12 +- state/exchange/selectors.ts | 6 +- state/exchange/types.ts | 3 +- state/futures/actions.ts | 821 +++++++++++++++++- state/futures/hooks.ts | 63 ++ state/futures/reducer.ts | 418 ++++++++- state/futures/selectors.ts | 446 +++++++++- state/futures/types.ts | 177 +++- state/hooks.ts | 128 ++- state/store.ts | 2 + state/types.ts | 5 + state/wallet/actions.ts | 17 +- state/wallet/reducer.ts | 5 +- state/wallet/selectors.ts | 5 + store/futures/index.ts | 302 +------ utils/__tests__/futures.test.ts | 7 +- utils/balances.ts | 87 +- utils/formatters/number.ts | 4 + utils/futures.ts | 171 +++- utils/queries.ts | 36 + 119 files changed, 4123 insertions(+), 3615 deletions(-) create mode 100644 hooks/useMonitorTransactions.ts delete mode 100644 queries/futures/useGetAverageFundingRateForMarket.ts delete mode 100644 queries/futures/useGetAverageFundingRateForMarkets.ts delete mode 100644 queries/futures/useGetCrossMarginAccountOverview.ts delete mode 100644 queries/futures/useGetCrossMarginSettings.ts delete mode 100644 queries/futures/useGetFuturesMarkets.ts delete mode 100644 queries/futures/useGetFuturesOpenOrders.ts delete mode 100644 queries/futures/useGetFuturesPositionForMarket.ts delete mode 100644 queries/futures/useGetFuturesPositionForMarkets.ts delete mode 100644 queries/futures/useGetFuturesPotentialTradeDetails.ts delete mode 100644 queries/futures/useGetFuturesVolumes.ts delete mode 100644 queries/synths/useSynthBalances.ts delete mode 100644 queries/walletBalances/types.ts rename queries/futures/useGetCrossMarginTradePreview.ts => sdk/contracts/FuturesMarketInternal.ts (84%) create mode 100644 sdk/types/tokens.ts delete mode 100644 sections/futures/MobileTrade/drawers/TradeConfirmationDrawer.tsx rename sections/futures/TradeCrossMargin/{EditLeverageModal.tsx => EditCrossMarginLeverageModal.tsx} (81%) create mode 100644 state/app/helpers.ts create mode 100644 state/app/reducer.ts create mode 100644 state/app/selectors.ts create mode 100644 state/app/types.ts create mode 100644 state/futures/hooks.ts create mode 100644 utils/queries.ts diff --git a/components/Loader/Loader.tsx b/components/Loader/Loader.tsx index c6abeaf7ae..88cfa57aac 100644 --- a/components/Loader/Loader.tsx +++ b/components/Loader/Loader.tsx @@ -30,4 +30,8 @@ export const MiniLoader = () => { return ; }; +export const ButtonLoader = () => { + return ; +}; + export default Loader; diff --git a/components/TVChart/TVChart.tsx b/components/TVChart/TVChart.tsx index eeffcc20b4..5e6de8fb1b 100644 --- a/components/TVChart/TVChart.tsx +++ b/components/TVChart/TVChart.tsx @@ -6,7 +6,7 @@ import { ThemeContext } from 'styled-components'; import { chain } from 'wagmi'; import Connector from 'containers/Connector'; -import { FuturesOrder } from 'queries/futures/types'; +import { FuturesOrder } from 'sdk/types/futures'; import { ChartBody } from 'sections/exchange/TradeCard/Charts/common/styles'; import { currentThemeState } from 'store/ui'; import darkTheme from 'styles/theme/colors/dark'; @@ -88,9 +88,9 @@ export function TVChart({ }; const renderOrderLines = () => { - clearOrderLines(); _widget.current?.onChartReady(() => { _widget.current?.chart().dataReady(() => { + clearOrderLines(); _oderLineRefs.current = openOrders.reduce((acc, order) => { if (order.targetPrice) { const color = diff --git a/constants/queryKeys.ts b/constants/queryKeys.ts index b3da648a3c..8d2f932afd 100644 --- a/constants/queryKeys.ts +++ b/constants/queryKeys.ts @@ -167,7 +167,6 @@ export const QUERY_KEYS = { networkId, currencyKey, ], - Markets: (networkId: NetworkId) => ['futures', 'marketsSummaries', networkId], Market: (networkId: NetworkId, currencyKey: string | null) => [ 'futures', currencyKey, @@ -215,18 +214,7 @@ export const QUERY_KEYS = { networkId, currencyKey, ], - FundingRates: ( - networkId: NetworkId, - periodLength: number, - marketAssets: FuturesMarketAsset[] - ) => ['futures', 'fundingRates', networkId, periodLength, marketAssets], TradingVolumeForAll: (networkId: NetworkId) => ['futures', 'tradingVolumeForAll', networkId], - AllPositionHistory: (networkId: NetworkId, walletAddress: string) => [ - 'futures', - 'allPositionHistory', - networkId, - walletAddress, - ], Position: (networkId: NetworkId, market: string | null, walletAddress: string) => [ 'futures', 'position', @@ -234,27 +222,6 @@ export const QUERY_KEYS = { market, walletAddress, ], - MarketsPositions: ( - networkId: NetworkId, - markets: string[] | [], - walletAddress: string, - crossMarginAddress: string - ) => ['futures', 'marketsPositions', networkId, markets, walletAddress, crossMarginAddress], - Portfolio: ( - networkId: NetworkId, - markets: string[] | [], - walletAddress: string | null, - crossMarginAddress: string | null, - freeMargin: number - ) => [ - 'futures', - 'positions', - networkId, - markets, - walletAddress, - crossMarginAddress, - freeMargin, - ], PositionHistory: (walletAddress: string | null, networkId: NetworkId) => [ 'futures', 'accountPositions', @@ -318,13 +285,6 @@ export const QUERY_KEYS = { currencyKey: string | null ) => ['futures', 'currentRoundId', networkId, walletAddress, currencyKey], OverviewStats: (networkId: NetworkId) => ['futures', 'overview-stats', networkId], - CrossMarginAccountOverview: (networkId: NetworkId, wallet: string, retryCount: number) => [ - 'futures', - 'cross-margin-account-overview', - networkId, - wallet, - retryCount, - ], CrossMarginSettings: (networkId: NetworkId, settingsAddress: string) => [ 'futures', 'cross-margin-settings', diff --git a/containers/Connector/Connector.tsx b/containers/Connector/Connector.tsx index c85edfdc92..d7132ed1b5 100644 --- a/containers/Connector/Connector.tsx +++ b/containers/Connector/Connector.tsx @@ -63,7 +63,7 @@ const useConnector = () => { setProviderReady(true); }); transactionNotifier = new BaseTN(provider); - }, [provider, dispatch, handleNetworkChange]); + }, [provider, handleNetworkChange]); useEffect(() => { handleNetworkChange(network.id as NetworkId); diff --git a/contexts/RefetchContext.tsx b/contexts/RefetchContext.tsx index a675a6d268..18a0680c83 100644 --- a/contexts/RefetchContext.tsx +++ b/contexts/RefetchContext.tsx @@ -1,20 +1,19 @@ -import React, { useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import React from 'react'; +import { useRecoilValue } from 'recoil'; -import useGetAverageFundingRateForMarkets from 'queries/futures/useGetAverageFundingRateForMarkets'; -import useGetCrossMarginAccountOverview from 'queries/futures/useGetCrossMarginAccountOverview'; -import useGetCrossMarginSettings from 'queries/futures/useGetCrossMarginSettings'; -import useGetFuturesOpenOrders from 'queries/futures/useGetFuturesOpenOrders'; -import useGetFuturesPositionForMarket from 'queries/futures/useGetFuturesPositionForMarket'; -import useGetFuturesPositionForMarkets from 'queries/futures/useGetFuturesPositionForMarkets'; import useGetFuturesPositionHistory from 'queries/futures/useGetFuturesPositionHistory'; -import useGetFuturesVolumes from 'queries/futures/useGetFuturesVolumes'; import useQueryCrossMarginAccount from 'queries/futures/useQueryCrossMarginAccount'; import useLaggedDailyPrice from 'queries/rates/useLaggedDailyPrice'; -import useSynthBalances from 'queries/synths/useSynthBalances'; -import { Period } from 'sdk/constants/period'; -import { futuresAccountState, futuresAccountTypeState, positionState } from 'store/futures'; -import logError from 'utils/logError'; +import { fetchBalances } from 'state/balances/actions'; +import { + fetchCrossMarginBalanceInfo, + fetchFuturesPositionsForType, + fetchOpenOrders, +} from 'state/futures/actions'; +import { selectFuturesType } from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; +import { futuresAccountState } from 'store/futures'; +import { refetchWithComparator } from 'utils/queries'; type RefetchType = | 'modify-position' @@ -24,10 +23,7 @@ type RefetchType = | 'account-margin-change' | 'cross-margin-account-change'; -type RefetchUntilType = - | 'wallet-balance-change' - | 'cross-margin-account-change' - | 'account-margin-change'; +type RefetchUntilType = 'cross-margin-account-change'; type RefetchContextType = { handleRefetch: (refetchType: RefetchType, timeout?: number) => void; @@ -40,60 +36,42 @@ const RefetchContext = React.createContext({ }); export const RefetchProvider: React.FC = ({ children }) => { - const selectedAccountType = useRecoilValue(futuresAccountTypeState); + const selectedAccountType = useAppSelector(selectFuturesType); const { crossMarginAddress } = useRecoilValue(futuresAccountState); - const setPosition = useSetRecoilState(positionState); + const dispatch = useAppDispatch(); - const synthsBalancesQuery = useSynthBalances(); - const openOrdersQuery = useGetFuturesOpenOrders(); - const positionQuery = useGetFuturesPositionForMarket(); - const crossMarginAccountOverview = useGetCrossMarginAccountOverview(); - const positionsQuery = useGetFuturesPositionForMarkets(); const positionHistoryQuery = useGetFuturesPositionHistory(); const queryCrossMarginAccount = useQueryCrossMarginAccount(); - useGetAverageFundingRateForMarkets(Period.ONE_HOUR); useLaggedDailyPrice(); - useGetFuturesVolumes({ refetchInterval: 60000 }); - useGetCrossMarginSettings(); - - useEffect(() => { - if (positionQuery.error) { - setPosition(null); - } - }, [positionQuery.error, setPosition]); const handleRefetch = (refetchType: RefetchType, timeout?: number) => { setTimeout(() => { switch (refetchType) { case 'modify-position': - openOrdersQuery.refetch(); - positionsQuery.refetch(); + dispatch(fetchOpenOrders()); positionHistoryQuery.refetch(); + dispatch(fetchFuturesPositionsForType()); if (selectedAccountType === 'cross_margin') { - crossMarginAccountOverview.refetch(); + dispatch(fetchCrossMarginBalanceInfo()); } break; case 'new-order': - positionsQuery.refetch(); - openOrdersQuery.refetch(); + dispatch(fetchFuturesPositionsForType()); + dispatch(fetchOpenOrders()); break; case 'close-position': - positionQuery.refetch(); - positionsQuery.refetch(); + dispatch(fetchFuturesPositionsForType()); positionHistoryQuery.refetch(); - openOrdersQuery.refetch(); + dispatch(fetchOpenOrders()); break; case 'margin-change': - positionQuery.refetch(); - positionsQuery.refetch(); + dispatch(fetchFuturesPositionsForType()); positionHistoryQuery.refetch(); - openOrdersQuery.refetch(); - synthsBalancesQuery.refetch(); + dispatch(fetchBalances()); break; case 'account-margin-change': - crossMarginAccountOverview.refetch(); - synthsBalancesQuery.refetch(); + dispatch(fetchBalances()); break; case 'cross-margin-account-change': queryCrossMarginAccount(); @@ -104,23 +82,6 @@ export const RefetchProvider: React.FC = ({ children }) => { const refetchUntilUpdate = async (refetchType: RefetchUntilType) => { switch (refetchType) { - case 'account-margin-change': - return Promise.all([ - refetchWithComparator( - crossMarginAccountOverview.refetch, - crossMarginAccountOverview, - (prev, next) => - !next.data || - prev?.data?.freeMargin?.toString() === next?.data?.freeMargin?.toString() - ), - refetchWithComparator( - synthsBalancesQuery.refetch, - synthsBalancesQuery, - (prev, next) => - !next.data || - prev?.data.susdWalletBalance?.toString() === next?.data.susdWalletBalance?.toString() - ), - ]); case 'cross-margin-account-change': return refetchWithComparator( queryCrossMarginAccount, @@ -137,41 +98,6 @@ export const RefetchProvider: React.FC = ({ children }) => { ); }; -// Takes a comparitor which should return a bool condition to -// signal to continue retrying, comparing prev and new query result - -const refetchWithComparator = async ( - query: () => Promise, - existingResult: any, - comparator: (previous: any, current: any) => boolean, - interval = 1000, - max = 25 -) => { - return new Promise((res) => { - let count = 1; - - const refetch = async (existingResult: any) => { - const timeout = setTimeout(async () => { - if (count > max) { - clearTimeout(timeout); - logError('refetch timeout'); - res({ data: null, status: 'timeout' }); - } else { - const next = await query(); - count += 1; - if (!comparator(existingResult, next)) { - clearTimeout(timeout); - res({ data: next, status: 'complete' }); - } else { - refetch(next); - } - } - }, interval); - }; - refetch(existingResult); - }); -}; - export const useRefetchContext = () => { return React.useContext(RefetchContext); }; diff --git a/hooks/useAverageEntryPrice.ts b/hooks/useAverageEntryPrice.ts index c8bb2563bd..d1416eb376 100644 --- a/hooks/useAverageEntryPrice.ts +++ b/hooks/useAverageEntryPrice.ts @@ -1,12 +1,12 @@ import { useMemo } from 'react'; -import { useRecoilValue } from 'recoil'; import { PositionHistory, PositionSide } from 'queries/futures/types'; -import { potentialTradeDetailsState } from 'store/futures'; +import { selectTradePreview } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; // Used to calculate the new average entry price of a modified position const useAverageEntryPrice = (positionHistory?: PositionHistory) => { - const { data: previewTrade } = useRecoilValue(potentialTradeDetailsState); + const previewTrade = useAppSelector(selectTradePreview); return useMemo(() => { if (positionHistory && previewTrade) { diff --git a/hooks/useFuturesData.ts b/hooks/useFuturesData.ts index 31c49754d3..ef616cc08e 100644 --- a/hooks/useFuturesData.ts +++ b/hooks/useFuturesData.ts @@ -2,119 +2,101 @@ import useSynthetixQueries from '@synthetixio/queries'; import Wei, { wei } from '@synthetixio/wei'; import { ethers } from 'ethers'; import { formatBytes32String } from 'ethers/lib/utils'; -import { debounce } from 'lodash'; import { useRouter } from 'next/router'; import { useState, useEffect, useMemo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; +import { CROSS_MARGIN_ENABLED, DEFAULT_FUTURES_MARGIN_TYPE } from 'constants/defaults'; +import { CROSS_MARGIN_ORDER_TYPES, ISOLATED_MARGIN_ORDER_TYPES } from 'constants/futures'; +import Connector from 'containers/Connector'; +import { FuturesAccountType } from 'queries/futures/types'; +import { serializeGasPrice } from 'state/app/helpers'; +import { setGasPrice } from 'state/app/reducer'; +import { selectGasSpeed } from 'state/app/selectors'; +import { clearTradeInputs, editTradeSizeInput } from 'state/futures/actions'; +import { usePollMarketFuturesData } from 'state/futures/hooks'; import { - CROSS_MARGIN_ENABLED, - DEFAULT_FUTURES_MARGIN_TYPE, - DEFAULT_LEVERAGE, -} from 'constants/defaults'; + setDynamicFeeRate as setDynamicFeeRateRedux, + setFuturesAccountType, + setOrderType, +} from 'state/futures/reducer'; import { - CROSS_MARGIN_ORDER_TYPES, - ISOLATED_MARGIN_ORDER_TYPES, - ORDER_KEEPER_ETH_DEPOSIT, -} from 'constants/futures'; -import Connector from 'containers/Connector'; -import { useRefetchContext } from 'contexts/RefetchContext'; -import { monitorTransaction } from 'contexts/RelayerContext'; -import { KWENTA_TRACKING_CODE, ORDER_PREVIEW_ERRORS } from 'queries/futures/constants'; -import { PositionSide, FuturesTradeInputs, FuturesAccountType } from 'queries/futures/types'; -import useGetFuturesPotentialTradeDetails from 'queries/futures/useGetFuturesPotentialTradeDetails'; -import { setFuturesAccountType, setOrderType as setReduxOrderType } from 'state/futures/reducer'; -import { selectMarketAssetRate, selectMaxLeverage } from 'state/futures/selectors'; + selectCrossMarginBalanceInfo, + selectCrossMarginAccount, + selectMarketAssetRate, + selectPosition, + selectMaxLeverage, + selectAboveMaxLeverage, + selectCrossMarginSettings, + selectTradeSizeInputs, + selectCrossMarginOrderPrice, + selectCrossMarginSelectedLeverage, + selectFuturesType, + selectLeverageSide, + selectOrderType, + selectIsAdvancedOrder, + selectCrossMarginTradeFees, + selectDynamicFeeRate, + selectCrossMarginMarginDelta, +} from 'state/futures/selectors'; import { selectMarketAsset, selectMarketInfo } from 'state/futures/selectors'; import { useAppSelector, useAppDispatch } from 'state/hooks'; -import { - crossMarginMarginDeltaState, - tradeFeesState, - futuresAccountState, - leverageSideState, - orderTypeState, - positionState, - futuresTradeInputsState, - crossMarginSettingsState, - futuresAccountTypeState, - preferredLeverageState, - simulatedTradeState, - potentialTradeDetailsState, - futuresOrderPriceState, - orderFeeCapState, - isAdvancedOrderState, - aboveMaxLeverageState, - crossMarginAccountOverviewState, - dynamicFeeRateState, -} from 'store/futures'; +import { futuresAccountState, orderFeeCapState } from 'store/futures'; import { computeMarketFee } from 'utils/costCalculations'; -import { zeroBN, floorNumber, weiToString } from 'utils/formatters/number'; -import { calculateMarginDelta, getDisplayAsset, MarketKeyByAsset } from 'utils/futures'; +import { zeroBN } from 'utils/formatters/number'; +import { MarketKeyByAsset } from 'utils/futures'; import logError from 'utils/logError'; import useCrossMarginAccountContracts from './useCrossMarginContracts'; -import usePersistedRecoilState from './usePersistedRecoilState'; - -const ZERO_TRADE_INPUTS = { - nativeSize: '', - susdSize: '', - nativeSizeDelta: zeroBN, - susdSizeDelta: zeroBN, - leverage: '', -}; - -const ZERO_FEES = { - staticFee: zeroBN, - crossMarginFee: zeroBN, - dynamicFeeRate: zeroBN, - keeperEthDeposit: zeroBN, - limitStopOrderFee: zeroBN, - total: zeroBN, -}; const useFuturesData = () => { const router = useRouter(); - const { t } = useTranslation(); - const { defaultSynthetixjs: synthetixjs, network, provider } = Connector.useContainer(); - const { useSynthetixTxn } = useSynthetixQueries(); + const { defaultSynthetixjs: synthetixjs, network } = Connector.useContainer(); + const { crossMarginAvailable } = useRecoilValue(futuresAccountState); + usePollMarketFuturesData(); + const dispatch = useAppDispatch(); + const crossMarginAddress = useAppSelector(selectCrossMarginAccount); - const getPotentialTrade = useGetFuturesPotentialTradeDetails(); - const crossMarginAccountOverview = useRecoilValue(crossMarginAccountOverviewState); + const crossMarginBalanceInfo = useAppSelector(selectCrossMarginBalanceInfo); const { crossMarginAccountContract } = useCrossMarginAccountContracts(); - const { handleRefetch, refetchUntilUpdate } = useRefetchContext(); + + const gasSpeed = useAppSelector(selectGasSpeed); + + // TODO: Move to sdk and redux + const { useEthGasPriceQuery } = useSynthetixQueries(); + const ethGasPriceQuery = useEthGasPriceQuery(); + + useEffect(() => { + const price = ethGasPriceQuery.data?.[gasSpeed]; + if (price) { + dispatch(setGasPrice(serializeGasPrice(price))); + } + }, [ethGasPriceQuery.data, gasSpeed, dispatch]); const marketAsset = useAppSelector(selectMarketAsset); - const [tradeInputs, setTradeInputs] = useRecoilState(futuresTradeInputsState); - const setSimulatedTrade = useSetRecoilState(simulatedTradeState); - const [crossMarginMarginDelta, setCrossMarginMarginDelta] = useRecoilState( - crossMarginMarginDeltaState - ); - const [tradeFees, setTradeFees] = useRecoilState(tradeFeesState); - const [dynamicFeeRate, setDynamicFeeRate] = useRecoilState(dynamicFeeRateState); - const leverageSide = useRecoilValue(leverageSideState); - const [orderType, setOrderType] = useRecoilState(orderTypeState); + const crossMarginMarginDelta = useAppSelector(selectCrossMarginMarginDelta); + const tradeFees = useAppSelector(selectCrossMarginTradeFees); + const dynamicFeeRate = useAppSelector(selectDynamicFeeRate); + const leverageSide = useAppSelector(selectLeverageSide); + const orderType = useAppSelector(selectOrderType); const feeCap = useRecoilValue(orderFeeCapState); - const position = useRecoilValue(positionState); - const aboveMaxLeverage = useRecoilValue(aboveMaxLeverageState); + const position = useAppSelector(selectPosition); + const aboveMaxLeverage = useAppSelector(selectAboveMaxLeverage); const maxLeverage = useAppSelector(selectMaxLeverage); - const { crossMarginAvailable, crossMarginAddress } = useRecoilValue(futuresAccountState); - const { tradeFee: crossMarginTradeFee, stopOrderFee, limitOrderFee } = useRecoilValue( - crossMarginSettingsState + const tradeSizeInputs = useAppSelector(selectTradeSizeInputs); + const selectedLeverage = useAppSelector(selectCrossMarginSelectedLeverage); + const selectedAccountType = useAppSelector(selectFuturesType); + + const { tradeFee: crossMarginTradeFee, stopOrderFee, limitOrderFee } = useAppSelector( + selectCrossMarginSettings ); - const isAdvancedOrder = useRecoilValue(isAdvancedOrderState); + const isAdvancedOrder = useAppSelector(selectIsAdvancedOrder); const marketAssetRate = useAppSelector(selectMarketAssetRate); - const orderPrice = useRecoilValue(futuresOrderPriceState); - const setPotentialTradeDetails = useSetRecoilState(potentialTradeDetailsState); - const [selectedAccountType, setSelectedAccountType] = useRecoilState(futuresAccountTypeState); - const [preferredLeverage] = usePersistedRecoilState(preferredLeverageState); - const dispatch = useAppDispatch(); - + const orderPrice = useAppSelector(selectCrossMarginOrderPrice); const market = useAppSelector(selectMarketInfo); const [maxFee, setMaxFee] = useState(zeroBN); - const [error, setError] = useState(null); const tradePrice = useMemo(() => wei(isAdvancedOrder ? orderPrice || zeroBN : marketAssetRate), [ orderPrice, @@ -123,18 +105,13 @@ const useFuturesData = () => { ]); const crossMarginAccount = useMemo(() => { - return crossMarginAvailable ? { freeMargin: crossMarginAccountOverview.freeMargin } : null; - }, [crossMarginAccountOverview.freeMargin, crossMarginAvailable]); + return crossMarginAvailable ? { freeMargin: crossMarginBalanceInfo.freeMargin } : null; + }, [crossMarginBalanceInfo.freeMargin, crossMarginAvailable]); const freeMargin = useMemo(() => crossMarginAccount?.freeMargin ?? zeroBN, [ crossMarginAccount?.freeMargin, ]); - const selectedLeverage = useMemo(() => { - const leverage = preferredLeverage[marketAsset] || DEFAULT_LEVERAGE; - return String(Math.min(maxLeverage.toNumber(), Number(leverage))); - }, [preferredLeverage, marketAsset, maxLeverage]); - const remainingMargin: Wei = useMemo(() => { if (selectedAccountType === 'isolated_margin') { return position?.remainingMargin || zeroBN; @@ -144,21 +121,9 @@ const useFuturesData = () => { return positionMargin.add(accountMargin); }, [position?.remainingMargin, crossMarginAccount?.freeMargin, selectedAccountType]); - const clearTradePreview = useCallback(() => { - setPotentialTradeDetails({ - data: null, - status: 'idle', - error: null, - }); - setTradeFees(ZERO_FEES); - }, [setPotentialTradeDetails, setTradeFees]); - const resetTradeState = useCallback(() => { - setSimulatedTrade(ZERO_TRADE_INPUTS); - setTradeInputs(ZERO_TRADE_INPUTS); - setCrossMarginMarginDelta(zeroBN); - clearTradePreview(); - }, [setSimulatedTrade, setTradeInputs, clearTradePreview, setCrossMarginMarginDelta]); + dispatch(clearTradeInputs()); + }, [dispatch]); const maxUsdInputAmount = useMemo(() => { if (selectedAccountType === 'isolated_margin') { @@ -209,21 +174,6 @@ const useFuturesData = () => { } }, [orderType, limitOrderFee, stopOrderFee]); - const getCrossMarginEthBal = useCallback(async () => { - if (!crossMarginAddress) return zeroBN; - const bal = await provider.getBalance(crossMarginAddress); - return wei(bal); - }, [crossMarginAddress, provider]); - - const calculateCrossMarginFee = useCallback( - (susdSizeDelta: Wei) => { - if (orderType !== 'limit' && orderType !== 'stop market') return zeroBN; - const advancedOrderFeeRate = orderType === 'limit' ? limitOrderFee : stopOrderFee; - return susdSizeDelta.abs().mul(advancedOrderFeeRate); - }, - [orderType, stopOrderFee, limitOrderFee] - ); - const totalFeeRate = useCallback( async (sizeDelta: Wei) => { const staticRate = computeMarketFee(market, sizeDelta); @@ -235,226 +185,6 @@ const useFuturesData = () => { [market, crossMarginTradeFee, dynamicFeeRate, advancedOrderFeeRate] ); - const calculateFees = useCallback( - async (susdSizeDelta: Wei, nativeSizeDelta: Wei) => { - if (!synthetixjs) return ZERO_FEES; - - const susdSize = susdSizeDelta.abs(); - const staticRate = computeMarketFee(market, nativeSizeDelta); - const tradeFee = susdSize.mul(staticRate).add(susdSize.mul(dynamicFeeRate)); - - const currentDeposit = - orderType === 'limit' || orderType === 'stop market' - ? await getCrossMarginEthBal() - : zeroBN; - const requiredDeposit = currentDeposit.lt(ORDER_KEEPER_ETH_DEPOSIT) - ? ORDER_KEEPER_ETH_DEPOSIT.sub(currentDeposit) - : zeroBN; - - const crossMarginFee = - selectedAccountType === 'cross_margin' ? susdSize.mul(crossMarginTradeFee) : zeroBN; - const limitStopOrderFee = calculateCrossMarginFee(susdSizeDelta); - const tradeFeeWei = wei(tradeFee); - - const fees = { - staticFee: tradeFeeWei, - crossMarginFee: crossMarginFee, - dynamicFeeRate, - keeperEthDeposit: requiredDeposit, - limitStopOrderFee: limitStopOrderFee, - total: tradeFeeWei.add(crossMarginFee).add(limitStopOrderFee), - }; - setTradeFees(fees); - return fees; - }, - [ - synthetixjs, - market, - dynamicFeeRate, - orderType, - getCrossMarginEthBal, - selectedAccountType, - crossMarginTradeFee, - calculateCrossMarginFee, - setTradeFees, - ] - ); - - // eslint-disable-next-line - const debounceFetchPreview = useCallback( - debounce(async (nextTrade: FuturesTradeInputs, fromLeverage = false) => { - setError(null); - try { - const fees = await calculateFees(nextTrade.susdSizeDelta, nextTrade.nativeSizeDelta); - let nextMarginDelta = zeroBN; - if (selectedAccountType === 'cross_margin') { - nextMarginDelta = - nextTrade.nativeSizeDelta.abs().gt(0) || fromLeverage - ? await calculateMarginDelta(nextTrade, fees, position) - : zeroBN; - setCrossMarginMarginDelta(nextMarginDelta); - } - getPotentialTrade( - nextTrade.nativeSizeDelta, - nextMarginDelta, - Number(nextTrade.leverage), - nextTrade.orderPrice - ); - } catch (err) { - if (Object.values(ORDER_PREVIEW_ERRORS).includes(err.message)) { - setError(err.message); - } else { - setError(t('futures.market.trade.preview.error')); - } - clearTradePreview(); - logError(err); - } - }, 500), - [ - setError, - calculateFees, - getPotentialTrade, - calculateMarginDelta, - position, - orderPrice, - orderType, - selectedAccountType, - logError, - setCrossMarginMarginDelta, - ] - ); - - const onStagePositionChange = useCallback( - (trade: FuturesTradeInputs) => { - setTradeInputs(trade); - setSimulatedTrade(null); - debounceFetchPreview(trade); - }, - [setTradeInputs, setSimulatedTrade, debounceFetchPreview] - ); - - const onTradeAmountChange = useCallback( - ( - value: string, - usdPrice: Wei, - currencyType: 'usd' | 'native', - options?: { simulateChange?: boolean; crossMarginLeverage?: Wei } - ) => { - if (!value || usdPrice.eq(0)) { - resetTradeState(); - return; - } - const positiveTrade = leverageSide === PositionSide.LONG; - const nativeSize = currencyType === 'native' ? wei(value) : wei(value).div(usdPrice); - const usdSize = currencyType === 'native' ? usdPrice.mul(value) : wei(value); - const changeEnabled = remainingMargin.gt(0) && value !== ''; - const isolatedMarginLeverage = changeEnabled ? usdSize.div(remainingMargin) : zeroBN; - - const inputLeverage = - selectedAccountType === 'cross_margin' - ? options?.crossMarginLeverage ?? wei(selectedLeverage) - : isolatedMarginLeverage; - let leverage = remainingMargin.eq(0) ? zeroBN : inputLeverage; - leverage = maxLeverage.gt(leverage) ? leverage : maxLeverage; - - const newTradeInputs = { - nativeSize: changeEnabled ? weiToString(nativeSize) : '', - susdSize: changeEnabled ? weiToString(usdSize) : '', - nativeSizeDelta: positiveTrade ? nativeSize : nativeSize.neg(), - susdSizeDelta: positiveTrade ? usdSize : usdSize.neg(), - orderPrice: usdPrice, - leverage: String(floorNumber(leverage)), - }; - - if (options?.simulateChange) { - // Allows us to keep it snappy updating the input values - setSimulatedTrade(newTradeInputs); - } else { - onStagePositionChange(newTradeInputs); - } - }, - [ - remainingMargin, - maxLeverage, - selectedLeverage, - selectedAccountType, - leverageSide, - resetTradeState, - setSimulatedTrade, - onStagePositionChange, - ] - ); - - const onChangeOpenPosLeverage = useCallback( - async (leverage: number) => { - debounceFetchPreview( - { - leverage: String(leverage), - nativeSize: '0', - susdSize: '0', - susdSizeDelta: zeroBN, - nativeSizeDelta: zeroBN, - }, - true - ); - }, - [debounceFetchPreview] - ); - - const onLeverageChange = useCallback( - (leverage: number) => { - if (selectedAccountType === 'cross_margin') { - onTradeAmountChange('', tradePrice, 'usd', { - crossMarginLeverage: wei(leverage), - }); - } else { - if (leverage <= 0) { - resetTradeState(); - } else { - const newTradeSize = - marketAssetRate.eq(0) || remainingMargin.eq(0) - ? '' - : wei(leverage).mul(remainingMargin).div(marketAssetRate).toString(); - onTradeAmountChange(newTradeSize, tradePrice, 'native'); - } - } - }, - [ - remainingMargin, - marketAssetRate, - selectedAccountType, - tradePrice, - resetTradeState, - onTradeAmountChange, - ] - ); - - const onTradeOrderPriceChange = useCallback( - (price: string) => { - if (price && tradeInputs.susdSize) { - // Recalc the trade - onTradeAmountChange(tradeInputs.susdSize, wei(price), 'usd'); - } - }, - [tradeInputs, onTradeAmountChange] - ); - - const orderTxn = useSynthetixTxn( - `FuturesMarket${getDisplayAsset(marketAsset)}`, - orderType === 'next price' ? 'submitNextPriceOrderWithTracking' : 'modifyPositionWithTracking', - [tradeInputs.nativeSizeDelta.toBN(), KWENTA_TRACKING_CODE], - {}, - { - enabled: - selectedAccountType === 'isolated_margin' && - !!marketAsset && - !!tradeInputs.leverage && - Number(tradeInputs.leverage) >= 0 && - maxLeverage.gte(tradeInputs.leverage) && - !tradeInputs.nativeSizeDelta.eq(zeroBN), - } - ); - const submitCrossMarginOrder = useCallback( async (fromEditLeverage?: boolean, gasLimit?: Wei | null) => { if (!crossMarginAccountContract) return; @@ -463,7 +193,7 @@ const useFuturesData = () => { { marketKey: formatBytes32String(MarketKeyByAsset[marketAsset]), marginDelta: crossMarginMarginDelta.toBN(), - sizeDelta: tradeInputs.nativeSizeDelta.toBN(), + sizeDelta: tradeSizeInputs.nativeSizeDelta.toBN(), }, ]; return await crossMarginAccountContract.distributeMargin(newPosition, { @@ -475,7 +205,7 @@ const useFuturesData = () => { return await crossMarginAccountContract.placeOrderWithFeeCap( formatBytes32String(MarketKeyByAsset[marketAsset]), crossMarginMarginDelta.toBN(), - tradeInputs.nativeSizeDelta.toBN(), + tradeSizeInputs.nativeSizeDelta.toBN(), wei(orderPrice).toBN(), enumType, feeCap.toBN(), @@ -489,29 +219,11 @@ const useFuturesData = () => { orderType, feeCap, crossMarginMarginDelta, - tradeInputs.nativeSizeDelta, + tradeSizeInputs.nativeSizeDelta, tradeFees.keeperEthDeposit, ] ); - const submitIsolatedMarginOrder = useCallback(() => { - orderTxn.mutate(); - }, [orderTxn]); - - useEffect(() => { - if (orderTxn.hash) { - monitorTransaction({ - txHash: orderTxn.hash, - onTxConfirmed: () => { - resetTradeState(); - handleRefetch('modify-position'); - refetchUntilUpdate('account-margin-change'); - }, - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [orderTxn.hash]); - useEffect(() => { const getMaxFee = async () => { if (remainingMargin.eq(0) || tradePrice.eq(0)) { @@ -550,16 +262,14 @@ const useFuturesData = () => { useEffect(() => { if (selectedAccountType === 'cross_margin' && !CROSS_MARGIN_ORDER_TYPES.includes(orderType)) { - setOrderType('market'); - dispatch(setReduxOrderType('market')); + dispatch(setOrderType('market')); } else if ( selectedAccountType === 'isolated_margin' && !ISOLATED_MARGIN_ORDER_TYPES.includes(orderType) ) { - setOrderType('market'); - dispatch(setReduxOrderType('market')); + dispatch(setOrderType('market')); } - onTradeAmountChange(tradeInputs.susdSize, tradePrice, 'usd'); + editTradeSizeInput(tradeSizeInputs.susdSizeString, 'usd'); // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, selectedAccountType, orderType, network.id]); @@ -575,8 +285,8 @@ const useFuturesData = () => { }, [router.events, resetTradeState]); useEffect(() => { - if (tradeInputs.susdSizeDelta.eq(0)) return; - onTradeAmountChange(tradeInputs.susdSize, tradePrice, 'usd'); + if (tradeSizeInputs.susdSizeDelta.eq(0)) return; + editTradeSizeInput(tradeSizeInputs.susdSizeString, 'usd'); // Only want to react to leverage side change // eslint-disable-next-line react-hooks/exhaustive-deps }, [leverageSide]); @@ -589,7 +299,6 @@ const useFuturesData = () => { useEffect(() => { if (!CROSS_MARGIN_ENABLED) { - setSelectedAccountType(DEFAULT_FUTURES_MARGIN_TYPE); dispatch(setFuturesAccountType(DEFAULT_FUTURES_MARGIN_TYPE)); return; } @@ -600,7 +309,6 @@ const useFuturesData = () => { const accountType = ['cross_margin', 'isolated_margin'].includes(routerType) ? routerType : DEFAULT_FUTURES_MARGIN_TYPE; - setSelectedAccountType(accountType); dispatch(setFuturesAccountType(accountType)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, router.query.accountType]); @@ -608,32 +316,25 @@ const useFuturesData = () => { useEffect(() => { const getDynamicFee = async () => { if (!synthetixjs) return; + // TODO: Move to sdk const dynamicFeeRate = await synthetixjs.contracts.Exchanger.dynamicFeeRateForExchange( ethers.utils.formatBytes32String('sUSD'), ethers.utils.formatBytes32String(marketAsset) ); - setDynamicFeeRate(wei(dynamicFeeRate.feeRate)); + dispatch(setDynamicFeeRateRedux(wei(dynamicFeeRate.feeRate).toString())); }; getDynamicFee(); - }, [marketAsset, setDynamicFeeRate, synthetixjs]); + }, [marketAsset, synthetixjs, dispatch]); return { - onLeverageChange, - onTradeAmountChange, - submitIsolatedMarginOrder, submitCrossMarginOrder, resetTradeState, - onTradeOrderPriceChange, - onChangeOpenPosLeverage, marketAssetRate, position, market, - orderTxn, maxUsdInputAmount, tradeFees, selectedLeverage, - error, - debounceFetchPreview, tradePrice, }; }; diff --git a/hooks/useMonitorTransactions.ts b/hooks/useMonitorTransactions.ts new file mode 100644 index 0000000000..aad6f4bc53 --- /dev/null +++ b/hooks/useMonitorTransactions.ts @@ -0,0 +1,27 @@ +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; + +import { monitorTransaction } from 'contexts/RelayerContext'; +import { TransactionStatus } from 'sdk/types/common'; +import { handleTransactionError, updateTransactionStatus } from 'state/futures/reducer'; +import { selectFuturesTransaction } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; + +export default function useMonitorTransactions() { + const dispatch = useDispatch(); + const transaction = useAppSelector(selectFuturesTransaction); + + useEffect(() => { + if (transaction?.hash) { + monitorTransaction({ + txHash: transaction.hash, + onTxFailed: (err) => { + dispatch(handleTransactionError(err.failureReason ?? 'transaction_failed')); + }, + onTxConfirmed: () => { + dispatch(updateTransactionStatus(TransactionStatus.Confirmed)); + }, + }); + } + }, [transaction?.hash, dispatch]); +} diff --git a/pages/_app.tsx b/pages/_app.tsx index 9d1f466e52..01d66fc0c4 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -17,6 +17,7 @@ import { chain, WagmiConfig } from 'wagmi'; import Connector from 'containers/Connector'; import { chains, wagmiClient } from 'containers/Connector/config'; +import useMonitorTransactions from 'hooks/useMonitorTransactions'; import AcknowledgementModal from 'sections/app/AcknowledgementModal'; import Layout from 'sections/shared/Layout'; import SystemStatus from 'sections/shared/SystemStatus'; @@ -52,6 +53,7 @@ const InnerApp: FC = ({ Component, pageProps }: AppPropsWithLayout) => } = Connector.useContainer(); useAppData(providerReady); + useMonitorTransactions(); const getLayout = Component.getLayout || ((page) => page); diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index 6825a7f53e..f980e01361 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -4,14 +4,13 @@ import { useTranslation } from 'react-i18next'; import DashboardLayout from 'sections/dashboard/DashboardLayout'; import Overview from 'sections/dashboard/Overview'; import GitHashID from 'sections/shared/Layout/AppLayout/GitHashID'; -import { fetchMarkets } from 'state/futures/actions'; -import { usePollAction } from 'state/hooks'; +import { usePollDashboardFuturesData } from 'state/futures/hooks'; type DashboardComponent = React.FC & { getLayout: (page: HTMLElement) => JSX.Element }; const Dashboard: DashboardComponent = () => { const { t } = useTranslation(); - usePollAction(fetchMarkets); + usePollDashboardFuturesData(); return ( <> diff --git a/pages/dashboard/markets.tsx b/pages/dashboard/markets.tsx index ec44dcba74..8b77328469 100644 --- a/pages/dashboard/markets.tsx +++ b/pages/dashboard/markets.tsx @@ -1,6 +1,7 @@ import Head from 'next/head'; import { useTranslation } from 'react-i18next'; +import Connector from 'containers/Connector'; import DashboardLayout from 'sections/dashboard/DashboardLayout'; import Markets from 'sections/dashboard/Markets'; import GitHashID from 'sections/shared/Layout/AppLayout/GitHashID'; @@ -11,7 +12,8 @@ type MarketsProps = React.FC & { getLayout: (page: HTMLElement) => JSX.Element } const MarketsPage: MarketsProps = () => { const { t } = useTranslation(); - usePollAction(fetchMarkets); + const { network } = Connector.useContainer(); + usePollAction('fetchMarkets', fetchMarkets, { dependencies: [network.id] }); return ( <> diff --git a/pages/market.tsx b/pages/market.tsx index 60611de8d1..4c04fff15a 100644 --- a/pages/market.tsx +++ b/pages/market.tsx @@ -23,15 +23,10 @@ import TradeIsolatedMargin from 'sections/futures/Trade/TradeIsolatedMargin'; import TradeCrossMargin from 'sections/futures/TradeCrossMargin'; import AppLayout from 'sections/shared/Layout/AppLayout'; import GitHashID from 'sections/shared/Layout/AppLayout/GitHashID'; -import { fetchMarkets } from 'state/futures/actions'; import { setMarketAsset } from 'state/futures/reducer'; -import { selectMarketAsset } from 'state/futures/selectors'; -import { useAppDispatch, useAppSelector, usePollAction } from 'state/hooks'; -import { - futuresAccountState, - futuresAccountTypeState, - showCrossMarginOnboardState, -} from 'store/futures'; +import { selectFuturesType, selectMarketAsset } from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; +import { futuresAccountState, showCrossMarginOnboardState } from 'store/futures'; import { PageContent, FullHeightContainer, RightSideContent } from 'styles/common'; import { FuturesMarketAsset, MarketKeyByAsset } from 'utils/futures'; @@ -43,7 +38,6 @@ const Market: MarketComponent = () => { const { walletAddress } = Connector.useContainer(); const futuresData = useFuturesData(); const dispatch = useAppDispatch(); - usePollAction(fetchMarkets); const routerMarketAsset = router.query.asset as FuturesMarketAsset; @@ -93,7 +87,7 @@ function TradePanelDesktop({ walletAddress, account }: TradePanelProps) { const { handleRefetch } = useRefetchContext(); const router = useRouter(); const isL2 = useIsL2(); - const accountType = useRecoilValue(futuresAccountTypeState); + const accountType = useAppSelector(selectFuturesType); if (walletAddress && !isL2) { return ; diff --git a/queries/futures/types.ts b/queries/futures/types.ts index c766d5d40e..463ab41801 100644 --- a/queries/futures/types.ts +++ b/queries/futures/types.ts @@ -2,54 +2,8 @@ import { Balances } from '@synthetixio/queries'; import Wei from '@synthetixio/wei'; import { BigNumber } from 'ethers'; -import { PotentialTradeStatus } from 'sections/futures/types'; -import { FuturesMarketAsset, FuturesMarketKey } from 'utils/futures'; - -export type PositionDetail = { - remainingMargin: Wei; - accessibleMargin: Wei; - orderPending: boolean; - order: { - pending: boolean; - fee: Wei; - leverage: Wei; - }; - position: { - fundingIndex: Wei; - lastPrice: Wei; - size: Wei; - margin: Wei; - }; - accruedFunding: Wei; - notionalValue: Wei; - liquidationPrice: Wei; - profitLoss: Wei; -}; - -export type FuturesFilledPosition = { - canLiquidatePosition: boolean; - side: PositionSide; - notionalValue: T; - accruedFunding: T; - initialMargin: T; - profitLoss: T; - fundingIndex: number; - lastPrice: T; - size: T; - liquidationPrice: T; - initialLeverage: T; - leverage: T; - pnl: T; - pnlPct: T; - marginRatio: T; -}; - -export type FuturesPosition = { - asset: FuturesMarketAsset; - remainingMargin: T; - accessibleMargin: T; - position: FuturesFilledPosition | null; -}; +import { FuturesOrderTypeDisplay, FuturesPotentialTradeDetails } from 'sdk/types/futures'; +import { FuturesMarketAsset } from 'utils/futures'; export type FuturesOpenInterest = { asset: string; @@ -135,14 +89,6 @@ export type FuturesTradeWithPrice = { price: string; }; -// This type exists to rename enum types from the subgraph to display-friendly types -export type FuturesOrderTypeDisplay = - | 'Next Price' - | 'Limit' - | 'Stop Market' - | 'Market' - | 'Liquidation'; - export type FuturesTrade = { size: Wei; asset: string; @@ -159,33 +105,6 @@ export type FuturesTrade = { accountType: FuturesAccountType; }; -export type FuturesOrder = { - id: string; - account: string; - asset: FuturesMarketAsset; - market: string; - marketKey: FuturesMarketKey; - size: Wei; - targetPrice: Wei | null; - marginDelta: Wei; - targetRoundId: Wei | null; - timestamp: Wei; - orderType: FuturesOrderTypeDisplay; - sizeTxt?: string; - targetPriceTxt?: string; - side?: PositionSide; - isStale?: boolean; - isExecutable?: boolean; - isCancelling?: boolean; -}; - -export type FuturesVolumes = { - [asset: string]: { - volume: Wei; - trades: Wei; - }; -}; - export type FuturesStat = { account: string; pnlWithFeesPaid: Wei; @@ -224,22 +143,6 @@ export type FundingRates = { [key in FuturesMarketAsset]: Wei; }; -export type FuturesPotentialTradeDetails = { - size: Wei; - sizeDelta: Wei; - liqPrice: Wei; - margin: Wei; - price: Wei; - fee: Wei; - leverage: Wei; - notionalValue: Wei; - minInitialMargin: Wei; - side: PositionSide; - status: PotentialTradeStatus; - showStatus: boolean; - statusMessage: string; -}; - export type FuturesPotentialTradeDetailsQuery = { data: FuturesPotentialTradeDetails | null; error: string | null; @@ -257,7 +160,6 @@ type CrossMarginAccount = string; type FactoryAddress = string; export type CrossMarginAccounts = Record>; -export type FuturesPositionsState = Record; export type PositionHistoryState = Record; export type Portfolio = { total: Wei; diff --git a/queries/futures/useGetAverageFundingRateForMarket.ts b/queries/futures/useGetAverageFundingRateForMarket.ts deleted file mode 100644 index 4859802fac..0000000000 --- a/queries/futures/useGetAverageFundingRateForMarket.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import Wei, { wei } from '@synthetixio/wei'; -import request, { gql } from 'graphql-request'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useNetwork } from 'wagmi'; - -import QUERY_KEYS from 'constants/queryKeys'; -import useIsL2 from 'hooks/useIsL2'; -import { selectMarketInfo, selectMarketKey } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import logError from 'utils/logError'; - -import { FundingRateUpdate } from './types'; -import { getFuturesEndpoint, calculateFundingRate } from './utils'; - -const useGetAverageFundingRateForMarket = ( - periodLength: number, - options?: UseQueryOptions -) => { - const { chain: network } = useNetwork(); - const isL2 = useIsL2(); - const marketKey = useAppSelector(selectMarketKey); - const marketInfo = useAppSelector(selectMarketInfo); - const futuresEndpoint = getFuturesEndpoint(network?.id as NetworkId); - - const price = marketInfo?.price; - const currentFundingRate = marketInfo?.currentFundingRate; - const marketAddress = marketInfo?.market; - - return useQuery( - QUERY_KEYS.Futures.FundingRate(network?.id as NetworkId, marketKey || ''), - async () => { - if (!marketKey || !price || !marketInfo) return null; - const minTimestamp = Math.floor(Date.now() / 1000) - periodLength; - try { - const response: { string: FundingRateUpdate[] } = await request( - futuresEndpoint, - gql` - query fundingRateUpdates($market: String!, $minTimestamp: BigInt!) { - # last before timestamp - first: fundingRateUpdates( - first: 1 - where: { market: $market, timestamp_lt: $minTimestamp } - orderBy: sequenceLength - orderDirection: desc - ) { - timestamp - funding - } - - # first after timestamp - next: fundingRateUpdates( - first: 1 - where: { market: $market, timestamp_gt: $minTimestamp } - orderBy: sequenceLength - orderDirection: asc - ) { - timestamp - funding - } - - # latest update - latest: fundingRateUpdates( - first: 1 - where: { market: $market } - orderBy: sequenceLength - orderDirection: desc - ) { - timestamp - funding - } - } - `, - { market: marketAddress, minTimestamp: minTimestamp } - ); - const responseFilt = Object.values(response) - .filter((value: FundingRateUpdate[]) => value.length > 0) - .map((entry: FundingRateUpdate[]): FundingRateUpdate => entry[0]) - .sort((a: FundingRateUpdate, b: FundingRateUpdate) => a.timestamp - b.timestamp); - - return responseFilt && !!currentFundingRate - ? calculateFundingRate( - minTimestamp, - periodLength, - responseFilt, - price, - currentFundingRate - ) - : wei(0); - } catch (e) { - logError(e); - return null; - } - }, - { - enabled: isL2 && !!marketInfo && !!currentFundingRate, - ...options, - } - ); -}; - -export default useGetAverageFundingRateForMarket; diff --git a/queries/futures/useGetAverageFundingRateForMarkets.ts b/queries/futures/useGetAverageFundingRateForMarkets.ts deleted file mode 100644 index d7667b906e..0000000000 --- a/queries/futures/useGetAverageFundingRateForMarkets.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import Wei from '@synthetixio/wei'; -import request, { gql } from 'graphql-request'; -import { useTranslation } from 'react-i18next'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useSetRecoilState } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import { Period, PERIOD_IN_SECONDS } from 'sdk/constants/period'; -import { selectMarketAssets, selectMarkets } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { fundingRatesState } from 'store/futures'; -import { FuturesMarketKey, MarketKeyByAsset } from 'utils/futures'; -import logError from 'utils/logError'; - -import { FundingRateUpdate } from './types'; -import { getFuturesEndpoint, calculateFundingRate } from './utils'; - -type FundingRateInput = { - marketAddress: string | undefined; - marketKey: FuturesMarketKey; - price: Wei | undefined; - currentFundingRate: Wei | undefined; -}; - -export type FundingRateResponse = { - asset: FuturesMarketKey; - fundingTitle: string; - fundingRate: Wei | null; -}; - -const useGetAverageFundingRateForMarkets = ( - period: Period, - options?: UseQueryOptions -) => { - const { t } = useTranslation(); - const { network } = Connector.useContainer(); - - const futuresMarkets = useAppSelector(selectMarkets); - const marketAssets = useAppSelector(selectMarketAssets); - const futuresEndpoint = getFuturesEndpoint(network?.id as NetworkId); - const setFundingRates = useSetRecoilState(fundingRatesState); - - const fundingRateInputs: FundingRateInput[] = futuresMarkets.map( - ({ asset, market, price, currentFundingRate }) => { - return { - marketAddress: market, - marketKey: MarketKeyByAsset[asset], - price: price, - currentFundingRate: currentFundingRate, - }; - } - ); - - const periodLength = PERIOD_IN_SECONDS[period]; - - const periodTitle = - period === Period.ONE_HOUR - ? t('futures.market.info.hourly-funding') - : t('futures.market.info.fallback-funding'); - - return useQuery( - QUERY_KEYS.Futures.FundingRates(network?.id as NetworkId, periodLength, marketAssets), - async () => { - const minTimestamp = Math.floor(Date.now() / 1000) - periodLength; - - const fundingRateQueries = fundingRateInputs.map(({ marketAddress, marketKey }) => { - return gql` - # last before timestamp - ${marketKey}_first: fundingRateUpdates( - first: 1 - where: { market: "${marketAddress}", timestamp_lt: $minTimestamp } - orderBy: sequenceLength - orderDirection: desc - ) { - timestamp - funding - } - - # first after timestamp - ${marketKey}_next: fundingRateUpdates( - first: 1 - where: { market: "${marketAddress}", timestamp_gt: $minTimestamp } - orderBy: sequenceLength - orderDirection: asc - ) { - timestamp - funding - } - - # latest update - ${marketKey}_latest: fundingRateUpdates( - first: 1 - where: { market: "${marketAddress}" } - orderBy: sequenceLength - orderDirection: desc - ) { - timestamp - funding - } - `; - }); - - try { - const marketFundingResponses: Record = await request( - futuresEndpoint, - gql` - query fundingRateUpdates($minTimestamp: BigInt!) { - ${fundingRateQueries.reduce((acc: string, curr: string) => { - return acc + curr; - })} - } - `, - { minTimestamp: minTimestamp } - ); - - const fundingRateResponses = fundingRateInputs.map( - ({ marketKey, currentFundingRate, price }) => { - if (!price) return null; - const marketResponses = [ - marketFundingResponses[`${marketKey}_first`], - marketFundingResponses[`${marketKey}_next`], - marketFundingResponses[`${marketKey}_latest`], - ]; - - const responseFilt = marketResponses - .filter((value: FundingRateUpdate[]) => value.length > 0) - .map((entry: FundingRateUpdate[]): FundingRateUpdate => entry[0]) - .sort((a: FundingRateUpdate, b: FundingRateUpdate) => a.timestamp - b.timestamp); - - const fundingRate = - responseFilt && !!currentFundingRate - ? calculateFundingRate( - minTimestamp, - periodLength, - responseFilt, - price, - currentFundingRate - ) - : currentFundingRate ?? null; - - const fundingPeriod = - responseFilt && !!currentFundingRate - ? periodTitle - : t('futures.markets.info.instant-funding'); - - const fundingRateResponse: FundingRateResponse = { - asset: marketKey, - fundingTitle: fundingPeriod, - fundingRate: fundingRate, - }; - return fundingRateResponse; - } - ); - - const fundingRates: FundingRateResponse[] = fundingRateResponses.filter( - (funding): funding is FundingRateResponse => !!funding - ); - - setFundingRates(fundingRates); - } catch (e) { - logError(e); - return null; - } - }, - { - enabled: futuresMarkets.length > 0 && futuresMarkets.length === marketAssets.length, - ...options, - } - ); -}; - -export default useGetAverageFundingRateForMarkets; diff --git a/queries/futures/useGetCrossMarginAccountOverview.ts b/queries/futures/useGetCrossMarginAccountOverview.ts deleted file mode 100644 index 9132a16521..0000000000 --- a/queries/futures/useGetCrossMarginAccountOverview.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import { wei } from '@synthetixio/wei'; -import { useState } from 'react'; -import { useQuery } from 'react-query'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import useCrossMarginAccountContracts from 'hooks/useCrossMarginContracts'; -import useSUSDContract from 'hooks/useSUSDContract'; -import { setCrossMarginAccountOverview } from 'state/futures/reducer'; -import { useAppDispatch } from 'state/hooks'; -import { crossMarginAccountOverviewState, futuresAccountState } from 'store/futures'; -import { zeroBN } from 'utils/formatters/number'; -import logError from 'utils/logError'; - -export default function useGetCrossMarginAccountOverview() { - const { walletAddress, network, provider } = Connector.useContainer(); - const { crossMarginAddress } = useRecoilValue(futuresAccountState); - const setAccountOverview = useSetRecoilState(crossMarginAccountOverviewState); - - const { crossMarginAccountContract } = useCrossMarginAccountContracts(); - const [retryCount, setRetryCount] = useState(0); - const susdContract = useSUSDContract(); - const dispatch = useAppDispatch(); - - return useQuery( - QUERY_KEYS.Futures.CrossMarginAccountOverview( - network.id as NetworkId, - crossMarginAddress || '', - retryCount - ), - async () => { - if (!crossMarginAddress || !crossMarginAccountContract || !susdContract || !walletAddress) { - const overview = { - freeMargin: zeroBN, - keeperEthBal: zeroBN, - allowance: zeroBN, - }; - setAccountOverview(overview); - dispatch( - setCrossMarginAccountOverview({ freeMargin: '0', keeperEthBal: '0', allowance: '0' }) - ); - return overview; - } - - try { - const [freeMargin, keeperEthBal, allowance] = await Promise.all([ - crossMarginAccountContract.freeMargin(), - provider.getBalance(crossMarginAddress), - susdContract.allowance(walletAddress, crossMarginAccountContract.address), - ]); - - const overview = { - freeMargin: wei(freeMargin), - keeperEthBal: wei(keeperEthBal), - allowance: wei(allowance), - }; - setRetryCount(0); - setAccountOverview(overview); - dispatch( - setCrossMarginAccountOverview({ - freeMargin: overview.freeMargin.toString(), - keeperEthBal: overview.keeperEthBal.toString(), - allowance: overview.allowance.toString(), - }) - ); - - return overview; - } catch (err) { - // This a hacky workaround to deal with the delayed Metamask error - // which causes the logs query to fail on network switching - // https://github.com/MetaMask/metamask-extension/issues/13375#issuecomment-1046125113 - if (retryCount < 2) { - setRetryCount(retryCount + 1); - } - logError(err); - } - } - ); -} diff --git a/queries/futures/useGetCrossMarginSettings.ts b/queries/futures/useGetCrossMarginSettings.ts deleted file mode 100644 index eda4d172dd..0000000000 --- a/queries/futures/useGetCrossMarginSettings.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import { wei } from '@synthetixio/wei'; -import { useQuery } from 'react-query'; -import { useSetRecoilState } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import useCrossMarginAccountContracts from 'hooks/useCrossMarginContracts'; -import { crossMarginSettingsState } from 'store/futures'; -import { zeroBN } from 'utils/formatters/number'; -import logError from 'utils/logError'; - -const BPS_CONVERSION = 10000; - -export default function useGetCrossMarginSettings() { - const { network } = Connector.useContainer(); - const setCrossMarginSettings = useSetRecoilState(crossMarginSettingsState); - - const { crossMarginBaseSettings } = useCrossMarginAccountContracts(); - - return useQuery( - QUERY_KEYS.Futures.CrossMarginSettings( - network.id as NetworkId, - crossMarginBaseSettings?.address ?? '' - ), - async () => { - if (!crossMarginBaseSettings) { - return; - } - - try { - const [tradeFee, limitOrderFee, stopOrderFee] = await Promise.all([ - crossMarginBaseSettings?.tradeFee(), - crossMarginBaseSettings?.limitOrderFee(), - crossMarginBaseSettings?.stopOrderFee(), - ]); - const settings = { - tradeFee: tradeFee ? wei(tradeFee.toNumber() / BPS_CONVERSION) : zeroBN, - limitOrderFee: limitOrderFee ? wei(limitOrderFee.toNumber() / BPS_CONVERSION) : zeroBN, - stopOrderFee: stopOrderFee ? wei(stopOrderFee.toNumber() / BPS_CONVERSION) : zeroBN, - }; - setCrossMarginSettings(settings); - - return; - } catch (err) { - logError(err); - } - } - ); -} diff --git a/queries/futures/useGetFuturesMarginTransfers.ts b/queries/futures/useGetFuturesMarginTransfers.ts index 4abc1d2ca7..d73751d025 100644 --- a/queries/futures/useGetFuturesMarginTransfers.ts +++ b/queries/futures/useGetFuturesMarginTransfers.ts @@ -1,12 +1,12 @@ import { NetworkId } from '@synthetixio/contracts-interface'; import request, { gql } from 'graphql-request'; import { useQuery, UseQueryOptions } from 'react-query'; -import { useRecoilValue } from 'recoil'; import QUERY_KEYS from 'constants/queryKeys'; import Connector from 'containers/Connector'; import useIsL2 from 'hooks/useIsL2'; -import { futuresAccountTypeState, selectedFuturesAddressState } from 'store/futures'; +import { selectFuturesAccount, selectFuturesType } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { getDisplayAsset } from 'utils/futures'; import logError from 'utils/logError'; @@ -53,8 +53,8 @@ const useGetFuturesMarginTransfers = ( currencyKey: string | null, options?: UseQueryOptions ) => { - const selectedFuturesAddress = useRecoilValue(selectedFuturesAddressState); - const futuresAccountType = useRecoilValue(futuresAccountTypeState); + const selectedFuturesAddress = useAppSelector(selectFuturesAccount); + const futuresAccountType = useAppSelector(selectFuturesType); const { defaultSynthetixjs: synthetixjs, network, isWalletConnected } = Connector.useContainer(); const futuresEndpoint = getFuturesEndpoint(network?.id as NetworkId); const isL2 = useIsL2(); diff --git a/queries/futures/useGetFuturesMarkets.ts b/queries/futures/useGetFuturesMarkets.ts deleted file mode 100644 index 39ddb556f0..0000000000 --- a/queries/futures/useGetFuturesMarkets.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import { wei } from '@synthetixio/wei'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useRecoilState } from 'recoil'; -import { chain } from 'wagmi'; - -import QUERY_KEYS from 'constants/queryKeys'; -import ROUTES from 'constants/routes'; -import Connector from 'containers/Connector'; -import { FuturesClosureReason } from 'hooks/useFuturesMarketClosed'; -import useIsL2 from 'hooks/useIsL2'; -import { FuturesMarket } from 'sdk/types/futures'; -import { setFuturesMarkets as setReduxFuturesMarkets } from 'state/futures/reducer'; -import { serializeWeiObject } from 'state/helpers'; -import { useAppDispatch } from 'state/hooks'; -import { futuresMarketsState } from 'store/futures'; -import { zeroBN } from 'utils/formatters/number'; -import { - FuturesMarketAsset, - getMarketName, - MarketKeyByAsset, - marketsForNetwork, -} from 'utils/futures'; - -import { getReasonFromCode } from './utils'; - -const useGetFuturesMarkets = (options?: UseQueryOptions) => { - const { network: activeChain, defaultSynthetixjs, l2Synthetixjs } = Connector.useContainer(); - - const homepage = window.location.pathname === ROUTES.Home.Root; - const isL2 = useIsL2(); - const network = homepage || !isL2 ? chain.optimism : activeChain; - const synthetixjs = homepage || !isL2 ? l2Synthetixjs : defaultSynthetixjs; - const [, setFuturesMarkets] = useRecoilState(futuresMarketsState); - const dispatch = useAppDispatch(); - - return useQuery( - QUERY_KEYS.Futures.Markets(network?.id as NetworkId), - async () => { - if (!synthetixjs) { - setFuturesMarkets([]); - return null; - } - const enabledMarkets = marketsForNetwork(network.id as NetworkId); - - const { - contracts: { FuturesMarketData, FuturesMarketSettings, SystemStatus, ExchangeRates }, - utils, - } = synthetixjs!; - - const [markets, globals] = await Promise.all([ - FuturesMarketData.allMarketSummaries(), - FuturesMarketData.globals(), - ]); - - const filteredMarkets = markets.filter((m: any) => { - const asset = utils.parseBytes32String(m.asset) as FuturesMarketAsset; - const market = enabledMarkets.find((market) => { - return asset === market.asset; - }); - return !!market; - }); - - const assetKeys = filteredMarkets.map((m: any) => { - const asset = utils.parseBytes32String(m.asset) as FuturesMarketAsset; - return utils.formatBytes32String(MarketKeyByAsset[asset]); - }); - - const currentRoundIdPromises = Promise.all( - assetKeys.map((key: string) => ExchangeRates.getCurrentRoundId(key)) - ); - - const marketLimitPromises = Promise.all( - assetKeys.map((key: string) => FuturesMarketSettings.maxMarketValueUSD(key)) - ); - - const systemStatusPromise = await SystemStatus.getFuturesMarketSuspensions(assetKeys); - - const [currentRoundIds, marketLimits, { suspensions, reasons }] = await Promise.all([ - currentRoundIdPromises, - marketLimitPromises, - systemStatusPromise, - ]); - - const futuresMarkets = filteredMarkets.map( - ( - { - market, - asset, - currentFundingRate, - feeRates, - marketDebt, - marketSkew, - maxLeverage, - marketSize, - price, - }: FuturesMarket, - i: number - ): FuturesMarket => ({ - market, - marketKey: MarketKeyByAsset[utils.parseBytes32String(asset) as FuturesMarketAsset], - marketName: getMarketName(utils.parseBytes32String(asset) as FuturesMarketAsset), - asset: utils.parseBytes32String(asset) as FuturesMarketAsset, - assetHex: asset, - currentFundingRate: wei(currentFundingRate).neg(), - currentRoundId: wei(currentRoundIds[i], 0), - feeRates: { - makerFee: wei(feeRates.makerFee), - takerFee: wei(feeRates.takerFee), - makerFeeNextPrice: wei(feeRates.makerFeeNextPrice), - takerFeeNextPrice: wei(feeRates.takerFeeNextPrice), - }, - openInterest: { - shortPct: wei(marketSize).eq(0) - ? 0 - : wei(marketSize).sub(marketSkew).div('2').div(marketSize).toNumber(), - longPct: wei(marketSize).eq(0) - ? 0 - : wei(marketSize).add(marketSkew).div('2').div(marketSize).toNumber(), - shortUSD: wei(marketSize).eq(0) - ? zeroBN - : wei(marketSize).sub(marketSkew).div('2').mul(price), - longUSD: wei(marketSize).eq(0) - ? zeroBN - : wei(marketSize).add(marketSkew).div('2').mul(price), - }, - marketDebt: wei(marketDebt), - marketSkew: wei(marketSkew), - maxLeverage: wei(maxLeverage), - marketSize: wei(marketSize), - marketLimit: wei(marketLimits[i]), - price: wei(price), - minInitialMargin: wei(globals.minInitialMargin), - keeperDeposit: wei(globals.minKeeperFee), - isSuspended: suspensions[i], - marketClosureReason: getReasonFromCode(reasons[i]) as FuturesClosureReason, - }) - ); - setFuturesMarkets(futuresMarkets); - dispatch(setReduxFuturesMarkets(futuresMarkets.map(serializeWeiObject))); - return futuresMarkets; - }, - { - enabled: !!synthetixjs, - refetchInterval: 15000, - ...options, - } - ); -}; - -export default useGetFuturesMarkets; diff --git a/queries/futures/useGetFuturesOpenOrders.ts b/queries/futures/useGetFuturesOpenOrders.ts deleted file mode 100644 index bfca36cfb7..0000000000 --- a/queries/futures/useGetFuturesOpenOrders.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import request, { gql } from 'graphql-request'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useSetRecoilState, useRecoilValue } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import { selectMarkets } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { openOrdersState, selectedFuturesAddressState } from 'store/futures'; -import logError from 'utils/logError'; - -import { FuturesOrder } from './types'; -import { getFuturesEndpoint, mapFuturesOrders } from './utils'; - -const useGetFuturesOpenOrders = (options?: UseQueryOptions) => { - const selectedFuturesAddress = useRecoilValue(selectedFuturesAddressState); - const { network } = Connector.useContainer(); - const futuresEndpoint = getFuturesEndpoint(network?.id as NetworkId); - - const futuresMarkets = useAppSelector(selectMarkets); - const setOpenOrders = useSetRecoilState(openOrdersState); - - return useQuery( - QUERY_KEYS.Futures.OpenOrders(network?.id as NetworkId, selectedFuturesAddress), - async () => { - if (!selectedFuturesAddress) { - setOpenOrders([]); - return []; - } - try { - const response = await request( - futuresEndpoint, - gql` - query OpenOrders($account: String!) { - futuresOrders(where: { abstractAccount: $account, status: Pending }) { - id - account - size - market - asset - targetRoundId - marginDelta - targetPrice - timestamp - orderType - } - } - `, - { account: selectedFuturesAddress } - ); - - const openOrders: FuturesOrder[] = response - ? response.futuresOrders.map((o: any) => { - const marketInfo = futuresMarkets.find((m) => m.asset === o.asset); - return mapFuturesOrders(o, marketInfo); - }) - : []; - setOpenOrders(openOrders); - return openOrders; - } catch (e) { - logError(e); - return []; - } - }, - { - enabled: futuresMarkets.length > 0 && !!selectedFuturesAddress, - refetchInterval: 5000, - ...options, - } - ); -}; - -export default useGetFuturesOpenOrders; diff --git a/queries/futures/useGetFuturesPositionForMarket.ts b/queries/futures/useGetFuturesPositionForMarket.ts deleted file mode 100644 index 9bb573b523..0000000000 --- a/queries/futures/useGetFuturesPositionForMarket.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import { utils as ethersUtils } from 'ethers'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import useIsL2 from 'hooks/useIsL2'; -import { setPosition as setReduxPosition } from 'state/futures/reducer'; -import { selectMarketKey } from 'state/futures/selectors'; -import { serializeWeiObject } from 'state/helpers'; -import { useAppSelector, useAppDispatch } from 'state/hooks'; -import { positionState, selectedFuturesAddressState } from 'store/futures'; -import { MarketAssetByKey } from 'utils/futures'; - -import { FuturesPosition } from './types'; -import { mapFuturesPosition, getFuturesMarketContract } from './utils'; - -const useGetFuturesPositionForMarket = (options?: UseQueryOptions) => { - const { defaultSynthetixjs: synthetixjs, network } = Connector.useContainer(); - const isL2 = useIsL2(); - const selectedFuturesAddress = useRecoilValue(selectedFuturesAddressState); - const market = useAppSelector(selectMarketKey); - const setPosition = useSetRecoilState(positionState); - const dispatch = useAppDispatch(); - - return useQuery( - QUERY_KEYS.Futures.Position( - network?.id as NetworkId, - market || null, - selectedFuturesAddress || '' - ), - async () => { - if (!isL2 || !market || !selectedFuturesAddress || !synthetixjs) { - setPosition(null); - return null; - } - const { - contracts: { FuturesMarketData }, - } = synthetixjs; - - const [futuresPosition, canLiquidatePosition] = await Promise.all([ - FuturesMarketData.positionDetailsForMarketKey( - ethersUtils.formatBytes32String(market), - selectedFuturesAddress - ), - getFuturesMarketContract(market, synthetixjs!.contracts).canLiquidate( - selectedFuturesAddress - ), - ]); - - const position = mapFuturesPosition( - futuresPosition, - canLiquidatePosition, - MarketAssetByKey[market] - ); - - setPosition(position); - dispatch(setReduxPosition(serializeWeiObject(position))); - - return position; - }, - { - refetchInterval: 5000, - ...options, - } - ); -}; - -export default useGetFuturesPositionForMarket; diff --git a/queries/futures/useGetFuturesPositionForMarkets.ts b/queries/futures/useGetFuturesPositionForMarkets.ts deleted file mode 100644 index f831c73bf8..0000000000 --- a/queries/futures/useGetFuturesPositionForMarkets.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import { Provider, Contract } from 'ethcall'; -import { utils as ethersUtils } from 'ethers'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import useIsL2 from 'hooks/useIsL2'; -import FuturesMarketABI from 'sdk/contracts/abis/FuturesMarket.json'; -import FuturesMarketDataABI from 'sdk/contracts/abis/FuturesMarketData.json'; -import { selectMarkets } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { positionsState, futuresAccountState } from 'store/futures'; -import { MarketKeyByAsset } from 'utils/futures'; - -import { FuturesAccountTypes, PositionDetail } from './types'; -import { mapFuturesPosition } from './utils'; - -const DEFAULT_POSITIONS = { - [FuturesAccountTypes.ISOLATED_MARGIN]: [], - [FuturesAccountTypes.CROSS_MARGIN]: [], -}; - -const useGetFuturesPositionForMarkets = (options?: UseQueryOptions) => { - const { defaultSynthetixjs: synthetixjs, provider, network } = Connector.useContainer(); - const isL2 = useIsL2(); - - const futuresMarkets = useAppSelector(selectMarkets); - const setPositions = useSetRecoilState(positionsState); - const { walletAddress, crossMarginAddress, crossMarginAvailable, status } = useRecoilValue( - futuresAccountState - ); - const assets = futuresMarkets.map(({ asset }) => asset); - - return useQuery( - QUERY_KEYS.Futures.MarketsPositions( - network?.id as NetworkId, - assets || [], - walletAddress ?? '', - crossMarginAddress ?? '' - ), - async () => { - if (!isL2 || !provider || status !== 'complete') { - setPositions(DEFAULT_POSITIONS); - return; - } - - const ethcallProvider = new Provider(); - await ethcallProvider.init(provider); - - const { - contracts: { FuturesMarketData }, - } = synthetixjs!; - - const FMD = new Contract(FuturesMarketData.address, FuturesMarketDataABI); - - const positionCalls = []; - const liquidationCalls = []; - - // isolated margin - for (const { market, asset } of futuresMarkets) { - positionCalls.push( - FMD.positionDetailsForMarketKey( - ethersUtils.formatBytes32String(MarketKeyByAsset[asset]), - walletAddress - ) - ); - const marketContract = new Contract(market, FuturesMarketABI); - liquidationCalls.push(marketContract.canLiquidate(walletAddress)); - } - - // cross margin - if (crossMarginAvailable && crossMarginAddress) { - for (const { market, asset } of futuresMarkets) { - positionCalls.push( - FMD.positionDetailsForMarketKey( - ethersUtils.formatBytes32String(MarketKeyByAsset[asset]), - crossMarginAddress - ) - ); - const marketContract = new Contract(market, FuturesMarketABI); - liquidationCalls.push(marketContract.canLiquidate(crossMarginAddress)); - } - } - - const positionDetails = (await ethcallProvider.all(positionCalls)) as PositionDetail[]; - const canLiquidateState = (await ethcallProvider.all(liquidationCalls)) as boolean[]; - - // split isolated and cross margin results - const positionDetailsIsolated = positionDetails.slice(0, futuresMarkets.length); - const positionDetailsCross = positionDetails.slice( - futuresMarkets.length, - positionDetails.length - ); - - const canLiquidateStateIsolated = canLiquidateState.slice(0, futuresMarkets.length); - const canLiquidateStateCross = canLiquidateState.slice( - futuresMarkets.length, - canLiquidateState.length - ); - - // map the positions using the results - const isolatedPositions = positionDetailsIsolated - .map((position, ind) => { - const canLiquidate = canLiquidateStateIsolated[ind]; - const asset = assets[ind]; - return mapFuturesPosition(position, canLiquidate, asset); - }) - .filter(({ remainingMargin }) => remainingMargin.gt(0)); - - const crossPositions = positionDetailsCross - .map((position, ind) => { - const canLiquidate = canLiquidateStateCross[ind]; - const asset = assets[ind]; - return mapFuturesPosition(position, canLiquidate, asset); - }) - .filter(({ remainingMargin }) => remainingMargin.gt(0)); - - setPositions({ - [FuturesAccountTypes.ISOLATED_MARGIN]: isolatedPositions, - [FuturesAccountTypes.CROSS_MARGIN]: crossPositions, - }); - }, - { - ...options, - } - ); -}; - -export default useGetFuturesPositionForMarkets; diff --git a/queries/futures/useGetFuturesPotentialTradeDetails.ts b/queries/futures/useGetFuturesPotentialTradeDetails.ts deleted file mode 100644 index e729b19947..0000000000 --- a/queries/futures/useGetFuturesPotentialTradeDetails.ts +++ /dev/null @@ -1,165 +0,0 @@ -import Wei, { wei } from '@synthetixio/wei'; -import { useCallback } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; - -import Connector from 'containers/Connector'; -import useIsL2 from 'hooks/useIsL2'; -import { PotentialTradeStatus, POTENTIAL_TRADE_STATUS_TO_MESSAGE } from 'sections/futures/types'; -import { selectMarketAsset } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { - leverageSideState, - potentialTradeDetailsState, - futuresAccountTypeState, - selectedFuturesAddressState, - orderTypeState, - crossMarginAccountOverviewState, -} from 'store/futures'; -import logError from 'utils/logError'; - -import { FuturesPotentialTradeDetails } from './types'; -import useGetCrossMarginPotentialTrade from './useGetCrossMarginTradePreview'; -import { getFuturesMarketContract } from './utils'; - -const SUCCESS = 'Success'; -const UNKNOWN = 'Unknown'; - -const useGetFuturesPotentialTradeDetails = () => { - const selectedAccountType = useRecoilValue(futuresAccountTypeState); - const selectedFuturesAddress = useRecoilValue(selectedFuturesAddressState); - const { defaultSynthetixjs: synthetixjs } = Connector.useContainer(); - const isL2 = useIsL2(); - - const leverageSide = useRecoilValue(leverageSideState); - const marketAsset = useAppSelector(selectMarketAsset); - - const orderType = useRecoilValue(orderTypeState); - const { freeMargin } = useRecoilValue(crossMarginAccountOverviewState); - const setPotentialTradeDetails = useSetRecoilState(potentialTradeDetailsState); - - const getPreview = useGetCrossMarginPotentialTrade(marketAsset, selectedFuturesAddress); - - const generatePreview = useCallback( - async ( - nativeSizeDelta: Wei, - positionMarginDelta: Wei, - leverage: number, - orderPrice?: Wei - ): Promise => { - if ( - !synthetixjs || - !marketAsset || - (!nativeSizeDelta && selectedAccountType === 'isolated_margin') || - (!nativeSizeDelta && (!positionMarginDelta || positionMarginDelta.eq(0))) || - ((orderType === 'limit' || orderType === 'stop market') && orderPrice?.eq(0)) || - !isL2 || - !selectedFuturesAddress - ) { - return null; - } - - if (positionMarginDelta.gt(freeMargin)) { - throw new Error('insufficient_margin'); - } - - const { - contracts: { FuturesMarketData }, - } = synthetixjs!; - - const FuturesMarketContract = getFuturesMarketContract(marketAsset, synthetixjs!.contracts); - - const globals = await FuturesMarketData.globals(); - const preview = - selectedAccountType === 'cross_margin' - ? await getPreview( - nativeSizeDelta.toBN(), - wei(positionMarginDelta).toBN(), - orderPrice ? wei(orderPrice).toBN() : undefined - ) - : await FuturesMarketContract.postTradeDetails( - wei(nativeSizeDelta).toBN(), - selectedFuturesAddress - ); - - if (!preview) { - return null; - } - - if (nativeSizeDelta.eq(0) && positionMarginDelta.eq(0)) { - // Size and margin changed to zero before query completed - return null; - } - - const { fee, liqPrice, margin, price, size, status } = preview; - - const potentialTradeDetails = { - fee: wei(fee), - liqPrice: wei(liqPrice), - margin: wei(margin), - price: wei(price), - size: wei(size), - sizeDelta: nativeSizeDelta, - side: leverageSide, - leverage: wei(leverage ? leverage : 1), - notionalValue: wei(size).mul(wei(price)), - minInitialMargin: wei(globals.minInitialMargin), - status, - showStatus: status > 0, // 0 is success - statusMessage: getStatusMessage(status), - }; - - return potentialTradeDetails; - }, - [ - selectedFuturesAddress, - marketAsset, - selectedAccountType, - isL2, - leverageSide, - synthetixjs, - orderType, - freeMargin, - getPreview, - ] - ); - - const getTradeDetails = useCallback( - async (nativeSize: Wei, positionMarginDelta: Wei, leverage: number, orderPrice?: Wei) => { - try { - setPotentialTradeDetails({ - data: null, - status: 'fetching', - error: null, - }); - const data = await generatePreview(nativeSize, positionMarginDelta, leverage, orderPrice); - setPotentialTradeDetails({ data, status: 'complete', error: null }); - } catch (err) { - logError(err); - setPotentialTradeDetails({ - data: null, - status: 'error', - error: err.message, - }); - } - }, - [setPotentialTradeDetails, generatePreview] - ); - - return getTradeDetails; -}; - -const getStatusMessage = (status: PotentialTradeStatus): string => { - if (typeof status !== 'number') { - return UNKNOWN; - } - - if (status === 0) { - return SUCCESS; - } else if (PotentialTradeStatus[status]) { - return POTENTIAL_TRADE_STATUS_TO_MESSAGE[PotentialTradeStatus[status]]; - } else { - return UNKNOWN; - } -}; - -export default useGetFuturesPotentialTradeDetails; diff --git a/queries/futures/useGetFuturesTradesForAccount.ts b/queries/futures/useGetFuturesTradesForAccount.ts index 82db0b38b6..4f65675c83 100644 --- a/queries/futures/useGetFuturesTradesForAccount.ts +++ b/queries/futures/useGetFuturesTradesForAccount.ts @@ -1,13 +1,13 @@ import { NetworkId } from '@synthetixio/contracts-interface'; import { utils as ethersUtils } from 'ethers'; import { useQuery, UseQueryOptions } from 'react-query'; -import { useRecoilValue } from 'recoil'; import { DEFAULT_NUMBER_OF_TRADES } from 'constants/defaults'; import QUERY_KEYS from 'constants/queryKeys'; import Connector from 'containers/Connector'; import useIsL2 from 'hooks/useIsL2'; -import { futuresAccountTypeState } from 'store/futures'; +import { selectFuturesType } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import logError from 'utils/logError'; import { FuturesAccountType, getFuturesTrades } from './subgraph'; @@ -19,7 +19,7 @@ const useGetFuturesTradesForAccount = ( account?: string | null, options?: UseQueryOptions & { forceAccount: boolean } ) => { - const selectedAccountType = useRecoilValue(futuresAccountTypeState); + const selectedAccountType = useAppSelector(selectFuturesType); const { network, isWalletConnected } = Connector.useContainer(); const isL2 = useIsL2(); const futuresEndpoint = getFuturesEndpoint(network?.id as NetworkId); diff --git a/queries/futures/useGetFuturesVolumes.ts b/queries/futures/useGetFuturesVolumes.ts deleted file mode 100644 index 59ff7f4e28..0000000000 --- a/queries/futures/useGetFuturesVolumes.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useSetRecoilState } from 'recoil'; -import { chain, useNetwork } from 'wagmi'; - -import QUERY_KEYS from 'constants/queryKeys'; -import ROUTES from 'constants/routes'; -import useIsL2 from 'hooks/useIsL2'; -import { PERIOD_IN_SECONDS } from 'sdk/constants/period'; -import { futuresVolumesState } from 'store/futures'; -import { calculateTimestampForPeriod } from 'utils/formatters/date'; -import logError from 'utils/logError'; - -import { DAY_PERIOD, FUTURES_ENDPOINT_OP_MAINNET } from './constants'; -import { getFuturesAggregateStats } from './subgraph'; -import { FuturesVolumes } from './types'; -import { calculateVolumes, getFuturesEndpoint } from './utils'; - -const useGetFuturesVolumes = (options?: UseQueryOptions) => { - const homepage = window.location.pathname === ROUTES.Home.Root; - const { chain: activeChain } = useNetwork(); - const isL2 = useIsL2(); - const network = homepage || !isL2 ? chain.optimism : activeChain; - const futuresEndpoint = homepage - ? FUTURES_ENDPOINT_OP_MAINNET - : getFuturesEndpoint(network?.id as NetworkId); - const setFuturesVolumes = useSetRecoilState(futuresVolumesState); - - return useQuery( - QUERY_KEYS.Futures.TradingVolumeForAll(network?.id as NetworkId), - async () => { - try { - const minTimestamp = Math.floor(calculateTimestampForPeriod(DAY_PERIOD) / 1000); - const response = await getFuturesAggregateStats( - futuresEndpoint, - { - first: 999999, - where: { - period: `${PERIOD_IN_SECONDS.ONE_HOUR}`, - timestamp_gte: `${minTimestamp}`, - }, - }, - { - id: true, - asset: true, - volume: true, - trades: true, - timestamp: true, - period: true, - feesKwenta: true, - feesSynthetix: true, - feesCrossMarginAccounts: true, - } - ); - const futuresVolumes = response ? calculateVolumes(response) : {}; - setFuturesVolumes(futuresVolumes); - return futuresVolumes; - } catch (e) { - logError(e); - return null; - } - }, - { ...options } - ); -}; - -export default useGetFuturesVolumes; diff --git a/queries/futures/useQueryCrossMarginAccount.ts b/queries/futures/useQueryCrossMarginAccount.ts index 32a7592e42..fa988eb6f4 100644 --- a/queries/futures/useQueryCrossMarginAccount.ts +++ b/queries/futures/useQueryCrossMarginAccount.ts @@ -6,6 +6,9 @@ import { useRecoilState } from 'recoil'; import { CROSS_MARGIN_ACCOUNT_FACTORY } from 'constants/address'; import Connector from 'containers/Connector'; import usePersistedRecoilState from 'hooks/usePersistedRecoilState'; +import { setCrossMarginAccount } from 'state/futures/reducer'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; +import { selectWallet } from 'state/wallet/selectors'; import { crossMarginAccountsState, futuresAccountState } from 'store/futures'; import logError from 'utils/logError'; @@ -41,12 +44,13 @@ const queryAccountsFromSubgraph = async ( export default function useQueryCrossMarginAccount() { const { crossMarginContractFactory } = useCrossMarginAccountContracts(); - const { network, walletAddress, signer } = Connector.useContainer(); - + const { network, signer } = Connector.useContainer(); const [futuresAccount, setFuturesAccount] = useRecoilState(futuresAccountState); const [storedCrossMarginAccounts, setStoredCrossMarginAccount] = usePersistedRecoilState( crossMarginAccountsState ); + const dispatch = useAppDispatch(); + const walletAddress = useAppSelector(selectWallet); const [retryCount, setRetryCount] = useState(0); const handleAccountQuery = async () => { @@ -106,6 +110,7 @@ export default function useQueryCrossMarginAccount() { crossMarginAvailable: true, walletAddress, }); + dispatch(setCrossMarginAccount(existing)); return existing; } @@ -141,6 +146,7 @@ export default function useQueryCrossMarginAccount() { walletAddress, }; setFuturesAccount(accountState); + dispatch(setCrossMarginAccount(crossMarginAccount)); return crossMarginAccount; }; diff --git a/queries/futures/utils.ts b/queries/futures/utils.ts index b9e5e73f79..4df0364285 100644 --- a/queries/futures/utils.ts +++ b/queries/futures/utils.ts @@ -8,8 +8,8 @@ import { chain } from 'wagmi'; import { ETH_UNIT } from 'constants/network'; import { MarketClosureReason } from 'hooks/useMarketClosed'; import { SynthsTrades, SynthsVolumes } from 'queries/synths/type'; -import { FuturesMarket } from 'sdk/types/futures'; -import { formatCurrency, formatDollars, weiFromWei, zeroBN } from 'utils/formatters/number'; +import { FuturesMarket, FuturesOrder, FuturesOrderTypeDisplay } from 'sdk/types/futures'; +import { formatCurrency, formatDollars, weiFromWei } from 'utils/formatters/number'; import { FuturesMarketAsset, getDisplayAsset, @@ -20,7 +20,6 @@ import { import { SECONDS_PER_DAY, FUTURES_ENDPOINTS } from './constants'; import { CrossMarginAccountTransferResult, - FuturesAggregateStatResult, FuturesMarginTransferResult, FuturesOrderResult, FuturesOrderType, @@ -28,18 +27,13 @@ import { FuturesTradeResult, } from './subgraph'; import { - FuturesPosition, FuturesOpenInterest, FuturesOneMinuteStat, - PositionDetail, PositionSide, - FuturesVolumes, PositionHistory, FundingRateUpdate, FuturesTrade, MarginTransfer, - FuturesOrder, - FuturesOrderTypeDisplay, } from './types'; export const getFuturesEndpoint = (networkId: NetworkId): string => { @@ -54,55 +48,6 @@ export const getFuturesMarketContract = (asset: string | null, contracts: Contra return contract; }; -export const mapFuturesPosition = ( - positionDetail: PositionDetail, - canLiquidatePosition: boolean, - asset: FuturesMarketAsset -): FuturesPosition => { - const { - remainingMargin, - accessibleMargin, - position: { fundingIndex, lastPrice, size, margin }, - accruedFunding, - notionalValue, - liquidationPrice, - profitLoss, - } = positionDetail; - const initialMargin = wei(margin); - const pnl = wei(profitLoss).add(wei(accruedFunding)); - const pnlPct = initialMargin.gt(0) ? pnl.div(wei(initialMargin)) : wei(0); - return { - asset, - remainingMargin: wei(remainingMargin), - accessibleMargin: wei(accessibleMargin), - position: wei(size).eq(zeroBN) - ? null - : { - canLiquidatePosition: !!canLiquidatePosition, - side: wei(size).gt(zeroBN) ? PositionSide.LONG : PositionSide.SHORT, - notionalValue: wei(notionalValue).abs(), - accruedFunding: wei(accruedFunding), - initialMargin, - profitLoss: wei(profitLoss), - fundingIndex: Number(fundingIndex), - lastPrice: wei(lastPrice), - size: wei(size).abs(), - liquidationPrice: wei(liquidationPrice), - initialLeverage: initialMargin.gt(0) - ? wei(size).mul(wei(lastPrice)).div(initialMargin).abs() - : wei(0), - pnl, - pnlPct, - marginRatio: wei(notionalValue).eq(zeroBN) - ? zeroBN - : wei(remainingMargin).div(wei(notionalValue).abs()), - leverage: wei(remainingMargin).eq(zeroBN) - ? zeroBN - : wei(notionalValue).div(wei(remainingMargin)).abs(), - }, - }; -}; - const mapOrderType = (orderType: Partial): FuturesOrderTypeDisplay => { return orderType === 'NextPrice' ? 'Next Price' @@ -194,24 +139,6 @@ export const mapOpenInterest = async ( return openInterest; }; -export const calculateVolumes = ( - futuresHourlyStats: FuturesAggregateStatResult[] -): FuturesVolumes => { - const volumes: FuturesVolumes = futuresHourlyStats.reduce( - (acc: FuturesVolumes, { asset, volume, trades }) => { - return { - ...acc, - [asset]: { - volume: volume.div(ETH_UNIT).add(acc[asset]?.volume ?? 0), - trades: trades.add(acc[asset]?.trades ?? 0), - }, - }; - }, - {} - ); - return volumes; -}; - export const calculateTradeVolumeForAllSynths = (SynthTrades: SynthsTrades): SynthsVolumes => { const result = SynthTrades.synthExchanges .filter((i) => i.fromSynth !== null) diff --git a/queries/synths/useSynthBalances.ts b/queries/synths/useSynthBalances.ts deleted file mode 100644 index 2bd67b7021..0000000000 --- a/queries/synths/useSynthBalances.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { CurrencyKey, NetworkId } from '@synthetixio/contracts-interface'; -import { SynthBalancesMap } from '@synthetixio/queries'; -import { wei } from '@synthetixio/wei'; -import { ethers } from 'ethers'; -import orderBy from 'lodash/orderBy'; -import { useQuery, UseQueryOptions } from 'react-query'; -import { useRecoilState } from 'recoil'; - -import QUERY_KEYS from 'constants/queryKeys'; -import Connector from 'containers/Connector'; -import { SynthBalances } from 'queries/futures/types'; -import { balancesState } from 'store/futures'; -import { zeroBN } from 'utils/formatters/number'; - -import { notNill } from './utils'; - -type SynthBalancesTuple = [string[], ethers.BigNumber[], ethers.BigNumber[]]; - -const useSynthBalances = (options?: UseQueryOptions) => { - const { network, defaultSynthetixjs: synthetixjs, walletAddress } = Connector.useContainer(); - - const [, setBalances] = useRecoilState(balancesState); - - return useQuery( - QUERY_KEYS.Synths.Balances(network?.id as NetworkId, walletAddress), - async () => { - if (!synthetixjs) { - // This should never happen since the query is not enabled when synthetixjs is undefined - throw Error('synthetixjs is undefined'); - } - const balancesMap: SynthBalancesMap = {}; - const [ - currencyKeys, - synthsBalances, - synthsUSDBalances, - ]: SynthBalancesTuple = await synthetixjs.contracts.SynthUtil.synthsBalances(walletAddress); - - let totalUSDBalance = wei(0); - - currencyKeys.forEach((currencyKeyBytes32, idx) => { - const balance = wei(synthsBalances[idx]); - - // discard empty balances - if (balance.gt(0)) { - const synthName = ethers.utils.parseBytes32String(currencyKeyBytes32) as CurrencyKey; - const usdBalance = wei(synthsUSDBalances[idx]); - - balancesMap[synthName] = { - currencyKey: synthName, - balance, - usdBalance, - }; - - totalUSDBalance = totalUSDBalance.add(usdBalance); - } - }); - - const balances = { - balancesMap: balancesMap, - balances: orderBy( - Object.values(balancesMap).filter(notNill), - (balance) => balance.usdBalance.toNumber(), - 'desc' - ), - totalUSDBalance, - susdWalletBalance: balancesMap?.['sUSD']?.balance ?? zeroBN, - }; - setBalances(balances); - - return balances; - }, - { - enabled: !!synthetixjs && !!walletAddress, - ...options, - } - ); -}; - -export default useSynthBalances; diff --git a/queries/walletBalances/types.ts b/queries/walletBalances/types.ts deleted file mode 100644 index 8992488a8b..0000000000 --- a/queries/walletBalances/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NetworkId } from '@synthetixio/contracts-interface'; - -export type Token = { - address: string; - decimals: number; - logoURI: string; - name: string; - symbol: string; - chainId: NetworkId; - tags: string[]; -}; diff --git a/sdk/README.md b/sdk/README.md index eee31c9049..4003d47efa 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -39,9 +39,21 @@ The following tasks are expected to be completed before the SDK can be considere - [ ] Remove code that refers to `@synthetixio/*` packages. - [x] Add contract typings. - [x] Implement `Context` class. +- [ ] Cache redux state and hydrate on load - [ ] Ensure type correctness of all SDK methods. - [ ] Set up foundation for retries on select methods. - [ ] Set up service for interacting with our subgraphs. +- [ ] Remove Duplicated types and move most data types to sdk. +- [ ] Cache contract data where possible, especially settings, config etc which doesn't change very often +- [ ] Create a contracts class in sdk context where we can cache more dynamic contracts such as markets +- [ ] Ensure types are added to all redux actions and reducers +- [ ] Remove old unused code +- [ ] Ensure consistent logic patterns across various pages, sdk states and services +- [ ] Ensure all data is correctly refetched after some mutation e.g. polling for contract and subgraph changes after a transaction +- [ ] Consider experimenting with WebSockets for realtime data (again). +- [ ] Remove walletAddress from connector and change references to the redux state wallet, this means we're always taking the sdk as source of truth for the wallet and avoids race conditions where queries are attempted before the signer is set. +- [ ] Add query statuses for all key queries and create derived query statuses for components which rely on completion of multiple queries +- [ ] Make all sdk number params consistent, e.g. use Wei everywhere insreads of BigNumber or string - [ ] Remove Duplicated types. - [ ] Create a standard way of passing in numeric values (particularly amounts) to the SDK. Weigh pros and cons of (`Wei`, `ethers.BigNumber` and `string`). @@ -49,6 +61,7 @@ The following tasks are expected to be completed before the SDK can be considere - [ ] Refactor `handleExchange` method. - [ ] Rename quote/base to from/to. +- [ ] Update the write transactions to use the typed calls - [ ] Ensure all methods use from/to in correct order. - [ ] Experiment with exchange contexts (store an instance of from/to pairings, so that the client doesn't have to pass it every time). - [ ] Reduce number of queries, by storing more data in class instance. @@ -61,7 +74,7 @@ The following tasks are expected to be completed before the SDK can be considere - [ ] Separate methods for isolated and cross-margin accounts. - [ ] Implement methods for fetching orders, past trades and transfers from the subgraph. -- [ ] Consider experimenting with WebSockets for realtime data (again). +- [ ] Query cross margin accounts from sdk and cache them there. ## Kwenta Token diff --git a/sdk/constants/futures.ts b/sdk/constants/futures.ts index be666391f0..387f19184a 100644 --- a/sdk/constants/futures.ts +++ b/sdk/constants/futures.ts @@ -113,3 +113,5 @@ export const MAINNET_MARKETS = MARKETS_LIST.filter( export const TESTNET_MARKETS = MARKETS_LIST.filter( (m) => m.supports === 'testnet' || m.supports === 'both' ); + +export const BPS_CONVERSION = 10000; diff --git a/queries/futures/useGetCrossMarginTradePreview.ts b/sdk/contracts/FuturesMarketInternal.ts similarity index 84% rename from queries/futures/useGetCrossMarginTradePreview.ts rename to sdk/contracts/FuturesMarketInternal.ts index 0f1ef56c65..8f598725fe 100644 --- a/queries/futures/useGetCrossMarginTradePreview.ts +++ b/sdk/contracts/FuturesMarketInternal.ts @@ -1,15 +1,13 @@ -import { SynthetixJS } from '@synthetixio/contracts-interface'; -import { wei } from '@synthetixio/wei'; import BN from 'bn.js'; import { Provider, Contract as MultiCallContract } from 'ethcall'; import { BigNumber, ethers, Contract } from 'ethers'; import { formatBytes32String } from 'ethers/lib/utils'; -import { useCallback, useMemo } from 'react'; -import Connector from 'containers/Connector'; -import useIsL2 from 'hooks/useIsL2'; +import { KWENTA_TRACKING_CODE } from 'queries/futures/constants'; import FuturesMarket from 'sdk/contracts/abis/FuturesMarket.json'; -import { PotentialTradeStatus } from 'sections/futures/types'; +import { FuturesMarket__factory } from 'sdk/contracts/types'; +import { FuturesMarketKey, PotentialTradeStatus } from 'sdk/types/futures'; +import { sdk } from 'state/config'; import { zeroBN, ZERO_BIG_NUM, @@ -18,11 +16,6 @@ import { multiplyDecimal, divideDecimal, } from 'utils/formatters/number'; -import { FuturesMarketAsset, MarketKeyByAsset } from 'utils/futures'; -import logError from 'utils/logError'; - -import { KWENTA_TRACKING_CODE } from './constants'; -import { getFuturesMarketContract } from './utils'; // Need to recreate postTradeDetails from the contract here locally // so we can modify margin for use with cross margin @@ -51,45 +44,11 @@ type Position = { const ethcallProvider = new Provider(); -export default function useGetCrossMarginTradePreview( - marketAsset: FuturesMarketAsset, - address: string | null | undefined -) { - const { defaultSynthetixjs: synthetixjs, provider } = Connector.useContainer(); - const isL2 = useIsL2(); - - const contractInstance = useMemo(() => { - if (!synthetixjs || !provider || !address || !isL2) return null; - try { - return new FuturesMarketInternal(synthetixjs, provider, marketAsset, address); - } catch (err) { - logError(err); - return null; - } - }, [synthetixjs, provider, address, isL2, marketAsset]); - - const getPreview = useCallback( - async (sizeDelta: BigNumber, marginDelta: BigNumber, orderPrice?: BigNumber) => { - if (contractInstance) { - const sizeBN = wei(sizeDelta || 0).toBN(); - const marginBN = wei(marginDelta || 0).toBN(); - const res = await contractInstance.getTradePreview(sizeBN, marginBN, orderPrice); - return res; - } - }, - [contractInstance] - ); - - return getPreview; -} - class FuturesMarketInternal { - _synthetixjs: SynthetixJS; _provider: ethers.providers.Provider; _futuresMarketContract: Contract; - _futuresSettingsContract: Contract; + _futuresSettingsContract: Contract | undefined; _marketKeyBytes: string; - _account: string; _onChainData: { assetPrice: BigNumber; @@ -103,18 +62,15 @@ class FuturesMarketInternal { _cache: Record; constructor( - synthetixjs: SynthetixJS, provider: ethers.providers.Provider, - marketAsset: FuturesMarketAsset, - account: string + marketKey: FuturesMarketKey, + marketAddress: string ) { - this._synthetixjs = synthetixjs; this._provider = provider; - this._futuresMarketContract = getFuturesMarketContract(marketAsset, synthetixjs.contracts); - this._futuresSettingsContract = synthetixjs.contracts.FuturesMarketSettings; - this._marketKeyBytes = formatBytes32String(MarketKeyByAsset[marketAsset]); - this._account = account; + this._futuresMarketContract = FuturesMarket__factory.connect(marketAddress, provider); + this._futuresSettingsContract = sdk.context.contracts.FuturesMarketSettings; + this._marketKeyBytes = formatBytes32String(marketKey); this._cache = {}; this._onChainData = { assetPrice: BigNumber.from(0), @@ -127,6 +83,7 @@ class FuturesMarketInternal { } getTradePreview = async ( + account: string, sizeDelta: BigNumber, marginDelta: BigNumber, limitStopPrice?: BigNumber @@ -140,10 +97,10 @@ class FuturesMarketInternal { multiCallContract.assetPrice(), multiCallContract.marketSkew(), multiCallContract.marketSize(), - multiCallContract.accruedFunding(this._account), + multiCallContract.accruedFunding(account), multiCallContract.fundingSequenceLength(), multiCallContract.fundingLastRecomputed(), - multiCallContract.positions(this._account), + multiCallContract.positions(account), ]); this._onChainData = { @@ -186,7 +143,8 @@ class FuturesMarketInternal { tradeParams: TradeParams, marginDelta: BigNumber ) => { - const dynamicFee = await this._synthetixjs.contracts.Exchanger.dynamicFeeRateForExchange( + if (!sdk.context.contracts.Exchanger) throw new Error('Unsupported network'); + const dynamicFee = await sdk.context.contracts.Exchanger?.dynamicFeeRateForExchange( formatBytes32String('sUSD'), this._marketKeyBytes ); @@ -244,7 +202,11 @@ class FuturesMarketInternal { ); if (maxLeverage.add(UNIT_BIG_NUM.div(100)).lt(leverage.abs())) { - return { newPos: oldPos, fee: zeroBN, status: PotentialTradeStatus.MAX_LEVERAGE_EXCEEDED }; + return { + newPos: oldPos, + fee: ZERO_BIG_NUM, + status: PotentialTradeStatus.MAX_LEVERAGE_EXCEEDED, + }; } const maxMarketValueUSD = await this._getSetting('maxMarketValueUSD', [this._marketKeyBytes]); @@ -422,9 +384,12 @@ class FuturesMarketInternal { _getSetting = async (settingGetter: string, params: any[] = []) => { const cached = this._cache[settingGetter]; + if (!this._futuresSettingsContract) throw new Error('Unsupported network'); if (cached) return cached; const res = await this._futuresSettingsContract[settingGetter](...params); this._cache[settingGetter] = res; return res; }; } + +export default FuturesMarketInternal; diff --git a/sdk/contracts/constants.ts b/sdk/contracts/constants.ts index 8e68b29efb..2203b680c8 100644 --- a/sdk/contracts/constants.ts +++ b/sdk/contracts/constants.ts @@ -43,6 +43,11 @@ export const ADDRESSES: Record> = { 10: '0xaE55F163337A2A46733AA66dA9F35299f9A46e9e', 420: '0x0dde87714C3bdACB93bB1d38605aFff209a85998', }, + SUSD: { + 1: '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', + 10: '0x8c6f28f2F1A3C87F0f938b96d27520d9751ec8d9', + 420: '0xebaeaad9236615542844adc5c149f86c36ad1136', + }, Synthetix: { 1: '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 5: '0x51f44ca59b867E005e48FA573Cb8df83FC7f7597', diff --git a/sdk/contracts/index.ts b/sdk/contracts/index.ts index 6b914ee9fe..df632a6cbf 100644 --- a/sdk/contracts/index.ts +++ b/sdk/contracts/index.ts @@ -80,6 +80,9 @@ export const getContractsByNetwork = ( SynthSwap: ADDRESSES.SynthSwap[networkId] ? SynthSwap__factory.connect(ADDRESSES.SynthSwap[networkId], provider) : undefined, + SUSD: ADDRESSES.SUSD[networkId] + ? ERC20__factory.connect(ADDRESSES.SUSD[networkId], provider) + : undefined, CrossMarginAccountFactory: ADDRESSES.CrossMarginAccountFactory[networkId] ? CrossMarginAccountFactory__factory.connect( ADDRESSES.CrossMarginAccountFactory[networkId], diff --git a/sdk/services/exchange.ts b/sdk/services/exchange.ts index f330c9afb8..c32b203f7a 100644 --- a/sdk/services/exchange.ts +++ b/sdk/services/exchange.ts @@ -23,9 +23,9 @@ import { PriceResponse } from 'queries/coingecko/types'; import { KWENTA_TRACKING_CODE } from 'queries/futures/constants'; import { Rates } from 'queries/rates/types'; import { getProxySynthSymbol } from 'queries/synths/utils'; -import { Token } from 'queries/walletBalances/types'; import { getEthGasPrice } from 'sdk/common/gas'; import erc20Abi from 'sdk/contracts/abis/ERC20.json'; +import { Token } from 'sdk/types/tokens'; import { startInterval } from 'sdk/utils/interval'; import { newGetCoinGeckoPricesForCurrencies, @@ -1088,7 +1088,8 @@ export default class ExchangeService { // One idea is to create a "tokens" service that handles everything // related to 1inch tokens. - public async getTokenBalances(walletAddress: string) { + public async getTokenBalances(walletAddress: string): Promise { + if (!this.sdk.context.isMainnet) return {}; const filteredTokens = this.tokenList.filter( (t) => !FILTERED_TOKENS.includes(t.address.toLowerCase()) ); diff --git a/sdk/services/futures.ts b/sdk/services/futures.ts index 7642468065..a0e852ead4 100644 --- a/sdk/services/futures.ts +++ b/sdk/services/futures.ts @@ -1,11 +1,25 @@ -import { wei } from '@synthetixio/wei'; +import { NetworkId } from '@synthetixio/contracts-interface'; +import Wei, { wei } from '@synthetixio/wei'; +import { Contract as EthCallContract } from 'ethcall'; +import { BigNumber, ContractTransaction, ethers } from 'ethers'; import { formatBytes32String, parseBytes32String } from 'ethers/lib/utils'; import request, { gql } from 'graphql-request'; import KwentaSDK from 'sdk'; +import { DAY_PERIOD, KWENTA_TRACKING_CODE } from 'queries/futures/constants'; +import { getFuturesAggregateStats } from 'queries/futures/subgraph'; +import { mapFuturesOrders } from 'queries/futures/utils'; import { UNSUPPORTED_NETWORK } from 'sdk/common/errors'; +import { BPS_CONVERSION } from 'sdk/constants/futures'; import { Period, PERIOD_IN_SECONDS } from 'sdk/constants/period'; import { getContractsByNetwork } from 'sdk/contracts'; +import FuturesMarketABI from 'sdk/contracts/abis/FuturesMarket.json'; +import FuturesMarketInternal from 'sdk/contracts/FuturesMarketInternal'; +import { + CrossMarginBase__factory, + FuturesMarketData, + FuturesMarket__factory, +} from 'sdk/contracts/types'; import { NetworkOverrideOptions } from 'sdk/types/common'; import { FundingRateInput, @@ -13,80 +27,115 @@ import { FundingRateUpdate, FuturesMarket, FuturesMarketAsset, + FuturesMarketKey, + FuturesOrder, + FuturesVolumes, MarketClosureReason, + PositionDetail, + PositionSide, } from 'sdk/types/futures'; import { calculateFundingRate, + calculateVolumes, + formatPotentialTrade, getFuturesEndpoint, getMarketName, getReasonFromCode, + mapFuturesPosition, marketsForNetwork, } from 'sdk/utils/futures'; +import { calculateTimestampForPeriod } from 'utils/formatters/date'; import { MarketKeyByAsset } from 'utils/futures'; export default class FuturesService { private sdk: KwentaSDK; - private futuresGqlEndpoint: string; + public markets: FuturesMarket[] | undefined; + public internalFuturesMarkets: Partial< + Record + > = {}; constructor(sdk: KwentaSDK) { this.sdk = sdk; - this.futuresGqlEndpoint = getFuturesEndpoint(sdk.context.networkId); + } + + get futuresGqlEndpoint() { + return getFuturesEndpoint(this.sdk.context.networkId); + } + + private getInternalFuturesMarket(marketAddress: string, marketKey: FuturesMarketKey) { + let market = this.internalFuturesMarkets[this.sdk.context.networkId]?.[marketAddress]; + if (market) return market; + market = new FuturesMarketInternal(this.sdk.context.provider, marketKey, marketAddress); + this.internalFuturesMarkets = { + [this.sdk.context.networkId]: { + ...this.internalFuturesMarkets[this.sdk.context.networkId], + [marketAddress]: market, + }, + }; + + return market; } public async getMarkets(networkOverride?: NetworkOverrideOptions) { const enabledMarkets = marketsForNetwork( networkOverride?.networkId || this.sdk.context.networkId ); - const contracts = networkOverride && networkOverride?.networkId !== this.sdk.context.networkId ? getContractsByNetwork(networkOverride.networkId, networkOverride.provider) : this.sdk.context.contracts; - const { FuturesMarketSettings, SystemStatus, ExchangeRates, FuturesMarketData } = contracts; + const { SystemStatus } = contracts; + const { + FuturesMarketSettings, + ExchangeRates, + FuturesMarketData, + } = this.sdk.context.mutliCallContracts; if (!FuturesMarketData || !FuturesMarketSettings || !SystemStatus || !ExchangeRates) { throw new Error(UNSUPPORTED_NETWORK); } - const [markets, globals] = await Promise.all([ + const [markets, globals] = await this.sdk.context.multicallProvider.all([ FuturesMarketData.allMarketSummaries(), FuturesMarketData.globals(), ]); const filteredMarkets = markets.filter((m: any) => { - const asset = parseBytes32String(m.asset) as FuturesMarketAsset; + const marketKey = parseBytes32String(m.key) as FuturesMarketKey; const market = enabledMarkets.find((market) => { - return asset === market.asset; + return marketKey === market.key; }); return !!market; - }); + }) as FuturesMarketData.MarketSummaryStructOutput[]; - const assetKeys = filteredMarkets.map((m: any) => { - const asset = parseBytes32String(m.asset) as FuturesMarketAsset; - return formatBytes32String(MarketKeyByAsset[asset]); + const marketKeys = filteredMarkets.map((m: any) => { + return m.key; }); - const currentRoundIdPromises = Promise.all( - assetKeys.map((key: string) => ExchangeRates.getCurrentRoundId(key)) + const currentRoundIdCalls = marketKeys.map((key: string) => + ExchangeRates.getCurrentRoundId(key) ); - const marketLimitPromises = Promise.all( - assetKeys.map((key: string) => FuturesMarketSettings.maxMarketValueUSD(key)) + const marketLimitCalls = marketKeys.map((key: string) => + FuturesMarketSettings.maxMarketValueUSD(key) ); - const systemStatusPromise = await SystemStatus.getFuturesMarketSuspensions(assetKeys); - - const [currentRoundIds, marketLimits, { suspensions, reasons }] = await Promise.all([ - currentRoundIdPromises, - marketLimitPromises, - systemStatusPromise, + const responses = await this.sdk.context.multicallProvider.all([ + ...currentRoundIdCalls, + ...marketLimitCalls, ]); + const currentRoundIds = responses.slice(0, currentRoundIdCalls.length); + const marketLimits = responses.slice(currentRoundIdCalls.length); + + const { suspensions, reasons } = await SystemStatus.getFuturesMarketSuspensions(marketKeys); + const futuresMarkets = filteredMarkets.map( ( { market, + key, asset, currentFundingRate, feeRates, @@ -99,7 +148,7 @@ export default class FuturesService { i: number ): FuturesMarket => ({ market, - marketKey: MarketKeyByAsset[parseBytes32String(asset) as FuturesMarketAsset], + marketKey: parseBytes32String(key) as FuturesMarketKey, marketName: getMarketName(parseBytes32String(asset) as FuturesMarketAsset), asset: parseBytes32String(asset) as FuturesMarketAsset, assetHex: asset, @@ -140,6 +189,49 @@ export default class FuturesService { return futuresMarkets; } + // TODO: types + public async getFuturesPositions( + address: string, // Cross margin or EOA address + futuresMarkets: { asset: FuturesMarketAsset; marketKey: FuturesMarketKey; address: string }[] + ) { + const marketDataContract = this.sdk.context.mutliCallContracts.FuturesMarketData; + + if (!this.sdk.context.isL2 || !marketDataContract) { + throw new Error(UNSUPPORTED_NETWORK); + } + + const positionCalls = []; + const liquidationCalls = []; + + for (const { address: marketAddress, marketKey } of futuresMarkets) { + positionCalls.push( + marketDataContract.positionDetailsForMarketKey(formatBytes32String(marketKey), address) + ); + const marketContract = new EthCallContract(marketAddress, FuturesMarketABI); + liquidationCalls.push(marketContract.canLiquidate(address)); + } + + // TODO: Combine these two? + const positionDetails = (await this.sdk.context.multicallProvider.all( + positionCalls + )) as PositionDetail[]; + const canLiquidateState = (await this.sdk.context.multicallProvider.all( + liquidationCalls + )) as boolean[]; + + // map the positions using the results + const positions = positionDetails + .map((position, ind) => { + const canLiquidate = canLiquidateState[ind]; + const marketKey = futuresMarkets[ind].marketKey; + const asset = futuresMarkets[ind].asset; + return mapFuturesPosition(position, canLiquidate, asset, marketKey); + }) + .filter(({ remainingMargin }) => remainingMargin.gt(0)); + + return positions; + } + public async getAverageFundingRates(markets: FuturesMarket[], period: Period) { const fundingRateInputs: FundingRateInput[] = markets.map( ({ asset, market, price, currentFundingRate }) => { @@ -244,4 +336,200 @@ export default class FuturesService { return fundingRateResponses.filter((funding): funding is FundingRateResponse => !!funding); } + + public async getDailyVolumes(): Promise { + const minTimestamp = Math.floor(calculateTimestampForPeriod(DAY_PERIOD) / 1000); + const response = await getFuturesAggregateStats( + this.futuresGqlEndpoint, + { + first: 999999, + where: { + period: `${PERIOD_IN_SECONDS.ONE_HOUR}`, + timestamp_gte: `${minTimestamp}`, + }, + }, + { + id: true, + asset: true, + volume: true, + trades: true, + timestamp: true, + period: true, + feesCrossMarginAccounts: true, + feesKwenta: true, + feesSynthetix: true, + } + ); + return response ? calculateVolumes(response) : {}; + } + + public async getCrossMarginBalanceInfo(crossMarginAddress: string) { + const crossMarginAccountContract = CrossMarginBase__factory.connect( + crossMarginAddress, + this.sdk.context.provider + ); + const { SUSD } = this.sdk.context.contracts; + if (!SUSD) throw new Error(UNSUPPORTED_NETWORK); + + // TODO: EthCall + const [freeMargin, keeperEthBal, allowance] = await Promise.all([ + crossMarginAccountContract.freeMargin(), + this.sdk.context.provider.getBalance(crossMarginAddress), + SUSD.allowance(this.sdk.context.walletAddress, crossMarginAccountContract.address), + ]); + + return { + freeMargin: wei(freeMargin), + keeperEthBal: wei(keeperEthBal), + allowance: wei(allowance), + }; + } + + public async getOpenOrders(account: string, markets: FuturesMarket[]) { + const response = await request( + this.futuresGqlEndpoint, + gql` + query OpenOrders($account: String!) { + futuresOrders(where: { abstractAccount: $account, status: Pending }) { + id + account + size + market + asset + targetRoundId + marginDelta + targetPrice + timestamp + orderType + } + } + `, + { account: account } + ); + + const openOrders: FuturesOrder[] = response + ? response.futuresOrders.map((o: any) => { + const marketInfo = markets.find((m) => m.asset === o.asset); + return mapFuturesOrders(o, marketInfo); + }) + : []; + return openOrders; + } + + public async getCrossMarginSettings() { + const crossMarginBaseSettings = this.sdk.context.mutliCallContracts.CrossMarginBaseSettings; + if (!crossMarginBaseSettings) throw new Error(UNSUPPORTED_NETWORK); + + const [tradeFee, limitOrderFee, stopOrderFee] = await this.sdk.context.multicallProvider.all([ + crossMarginBaseSettings.tradeFee(), + crossMarginBaseSettings.limitOrderFee(), + crossMarginBaseSettings.stopOrderFee(), + ]); + return { + tradeFee: tradeFee ? wei(tradeFee.toNumber() / BPS_CONVERSION) : wei(0), + limitOrderFee: limitOrderFee ? wei(limitOrderFee.toNumber() / BPS_CONVERSION) : wei(0), + stopOrderFee: stopOrderFee ? wei(stopOrderFee.toNumber() / BPS_CONVERSION) : wei(0), + }; + } + + public async getIsolatedTradePreview( + marketAddress: string, + sizeDelta: Wei, + leverageSide: PositionSide + ) { + const market = FuturesMarket__factory.connect(marketAddress, this.sdk.context.signer); + const details = await market.postTradeDetails( + wei(sizeDelta).toBN(), + this.sdk.context.walletAddress + ); + return formatPotentialTrade(details, sizeDelta, leverageSide); + } + + public async getCrossMarginTradePreview( + crossMarginAccount: string, + marketKey: FuturesMarketKey, + marketAddress: string, + tradeParams: { + sizeDelta: Wei; + marginDelta: Wei; + orderPrice?: Wei; + leverageSide: PositionSide; + } + ) { + const marketInternal = this.getInternalFuturesMarket(marketAddress, marketKey); + + const preview = await marketInternal.getTradePreview( + crossMarginAccount, + tradeParams.sizeDelta.toBN(), + tradeParams.marginDelta.toBN(), + tradeParams.orderPrice?.toBN() + ); + + return formatPotentialTrade(preview, tradeParams.sizeDelta, tradeParams.leverageSide); + } + + public async getCrossMarginKeeperBalance(account: string) { + const bal = await this.sdk.context.provider.getBalance(account); + return wei(bal); + } + + // Contract mutations + + public async approveCrossMarginDeposit( + crossMarginAddress: string, + amount: BigNumber = ethers.constants.MaxUint256 + ) { + if (!this.sdk.context.contracts.SUSD) throw new Error(UNSUPPORTED_NETWORK); + return this.sdk.context.contracts.SUSD.approve(crossMarginAddress, amount); + } + + public async depositCrossMargin(crossMarginAddress: string, amount: Wei) { + // TODO: Store on instance + const crossMarginAccountContract = CrossMarginBase__factory.connect( + crossMarginAddress, + this.sdk.context.signer + ); + return crossMarginAccountContract.deposit(amount.toBN()); + } + + public async withdrawCrossMargin(crossMarginAddress: string, amount: Wei) { + // TODO: Store on instance + const crossMarginAccountContract = CrossMarginBase__factory.connect( + crossMarginAddress, + this.sdk.context.signer + ); + return crossMarginAccountContract.withdraw(amount.toBN()); + } + + public async depositIsolatedMargin(marketAddress: string, amount: Wei) { + const market = FuturesMarket__factory.connect(marketAddress, this.sdk.context.signer); + return market.transferMargin(amount.toBN()); + } + + public async withdrawIsolatedMargin(marketAddress: string, amount: Wei) { + const market = FuturesMarket__factory.connect(marketAddress, this.sdk.context.signer); + return market.transferMargin(amount.neg().toBN()); + } + + public async closeIsolatedPosition(marketAddress: string) { + const market = FuturesMarket__factory.connect(marketAddress, this.sdk.context.signer); + return market.closePositionWithTracking(KWENTA_TRACKING_CODE); + } + + public async modifyIsolatedMarginPosition( + marketAddress: string, + sizeDelta: Wei, + useNextPrice = false, + estimationOnly: T + ): TxReturn { + const market = FuturesMarket__factory.connect(marketAddress, this.sdk.context.signer); + const root = estimationOnly ? market.estimateGas : market; + return useNextPrice + ? (root.submitNextPriceOrderWithTracking(sizeDelta.toBN(), KWENTA_TRACKING_CODE) as any) + : (root.modifyPositionWithTracking(sizeDelta.toBN(), KWENTA_TRACKING_CODE) as any); + } } + +type TxReturn = Promise< + T extends true ? BigNumber : ContractTransaction +>; diff --git a/sdk/services/synths.ts b/sdk/services/synths.ts index b300f3a2ee..8d98f7816c 100644 --- a/sdk/services/synths.ts +++ b/sdk/services/synths.ts @@ -1,11 +1,11 @@ import { CurrencyKey } from '@synthetixio/contracts-interface'; -import { SynthBalancesMap } from '@synthetixio/queries'; import { wei } from '@synthetixio/wei'; import { ethers } from 'ethers'; import { orderBy } from 'lodash'; import KwentaSDK from 'sdk'; import { notNill } from 'queries/synths/utils'; +import { SynthBalance } from 'sdk/types/tokens'; import { zeroBN } from 'utils/formatters/number'; import * as sdkErrors from '../common/errors'; @@ -24,7 +24,7 @@ export default class SynthsService { throw new Error(sdkErrors.UNSUPPORTED_NETWORK); } - const balancesMap: SynthBalancesMap = {}; + const balancesMap: Record = {}; const [ currencyKeys, synthsBalances, diff --git a/sdk/types/common.ts b/sdk/types/common.ts index ef67ec1996..f3458b9bcf 100644 --- a/sdk/types/common.ts +++ b/sdk/types/common.ts @@ -5,3 +5,10 @@ export type NetworkOverrideOptions = { networkId: NetworkId; provider: providers.Provider; }; + +export enum TransactionStatus { + AwaitingExecution = 'AwaitingExecution', + Executed = 'Executed', + Confirmed = 'Confirmed', + Failed = 'Failed', +} diff --git a/sdk/types/futures.ts b/sdk/types/futures.ts index c05558c632..c82d2dac20 100644 --- a/sdk/types/futures.ts +++ b/sdk/types/futures.ts @@ -1,4 +1,5 @@ import Wei from '@synthetixio/wei'; +import { BigNumber } from 'ethers'; export type FundingRateInput = { marketAddress: string | undefined; @@ -17,7 +18,7 @@ export type MarketClosureReason = SynthSuspensionReason; export type FuturesMarket = { market: string; - marketKey?: FuturesMarketKey; + marketKey: FuturesMarketKey; marketName: string; asset: FuturesMarketAsset; assetHex: string; @@ -106,3 +107,130 @@ export interface FuturesMarketConfig { supports: 'mainnet' | 'testnet' | 'both'; disabled?: boolean; } + +export type FuturesVolumes = { + [asset: string]: { + volume: T; + trades: T; + }; +}; + +export type PositionDetail = { + remainingMargin: Wei; + accessibleMargin: Wei; + orderPending: boolean; + order: { + pending: boolean; + fee: Wei; + leverage: Wei; + }; + position: { + fundingIndex: Wei; + lastPrice: Wei; + size: Wei; + margin: Wei; + }; + accruedFunding: Wei; + notionalValue: Wei; + liquidationPrice: Wei; + profitLoss: Wei; +}; + +export enum PositionSide { + LONG = 'long', + SHORT = 'short', +} + +export type FuturesFilledPosition = { + canLiquidatePosition: boolean; + side: PositionSide; + notionalValue: T; + accruedFunding: T; + initialMargin: T; + profitLoss: T; + fundingIndex: number; + lastPrice: T; + size: T; + liquidationPrice: T; + initialLeverage: T; + leverage: T; + pnl: T; + pnlPct: T; + marginRatio: T; +}; + +export type FuturesPosition = { + asset: FuturesMarketAsset; + marketKey: FuturesMarketKey; + remainingMargin: T; + accessibleMargin: T; + position: FuturesFilledPosition | null; +}; + +// This type exists to rename enum types from the subgraph to display-friendly types +export type FuturesOrderTypeDisplay = + | 'Next Price' + | 'Limit' + | 'Stop Market' + | 'Market' + | 'Liquidation'; + +export type FuturesOrder = { + id: string; + account: string; + asset: FuturesMarketAsset; + market: string; + marketKey: FuturesMarketKey; + size: T; + targetPrice: T | null; + marginDelta: T; + targetRoundId: T | null; + timestamp: T; + orderType: FuturesOrderTypeDisplay; + sizeTxt?: string; + targetPriceTxt?: string; + side?: PositionSide; + isStale?: boolean; + isExecutable?: boolean; + isCancelling?: boolean; +}; + +export type FuturesPotentialTradeDetails = { + size: T; + sizeDelta: T; + liqPrice: T; + margin: T; + price: T; + fee: T; + leverage: T; + notionalValue: T; + side: PositionSide; + status: PotentialTradeStatus; + showStatus: boolean; + statusMessage: string; +}; + +// https://github.com/Synthetixio/synthetix/blob/4d2add4f74c68ac4f1106f6e7be4c31d4f1ccc76/contracts/interfaces/IFuturesMarketBaseTypes.sol#L6-L19 +export enum PotentialTradeStatus { + OK = 0, + INVALID_PRICE = 1, + PRICE_OUT_OF_BOUNDS = 2, + CAN_LIQUIDATE = 3, + CANNOT_LIQUIDATE = 4, + MAX_MARKET_SIZE_EXCEEDED = 5, + MAX_LEVERAGE_EXCEEDED = 6, + INSUFFICIENT_MARGIN = 7, + NOT_PERMITTED = 8, + NIL_ORDER = 9, + NO_POSITION_OPEN = 10, + PRICE_TOO_VOLATILE = 11, +} + +export type PostTradeDetailsResponse = { + margin: BigNumber; + size: BigNumber; + price: BigNumber; + liqPrice: BigNumber; + fee: BigNumber; + status: number; +}; diff --git a/sdk/types/tokens.ts b/sdk/types/tokens.ts new file mode 100644 index 0000000000..76d487c937 --- /dev/null +++ b/sdk/types/tokens.ts @@ -0,0 +1,28 @@ +import { NetworkId } from '@synthetixio/contracts-interface'; +import Wei from '@synthetixio/wei'; + +export type SynthBalance = { + currencyKey: string; + balance: T; + usdBalance: T; +}; + +export type TokenBalances = Partial< + Record< + string, + { + balance: T; + token: Token; + } + > +>; + +export type Token = { + address: string; + chainId: NetworkId; + decimals: number; + logoURI: string; + name: string; + symbol: string; + tags: string[]; +}; diff --git a/sdk/utils/futures.ts b/sdk/utils/futures.ts index 79d66c8970..221c4adcbc 100644 --- a/sdk/utils/futures.ts +++ b/sdk/utils/futures.ts @@ -1,9 +1,29 @@ import Wei, { wei } from '@synthetixio/wei'; import { BigNumber } from 'ethers'; +import { ETH_UNIT } from 'constants/network'; +import { FuturesAggregateStatResult } from 'queries/futures/subgraph'; import { FUTURES_ENDPOINTS, MAINNET_MARKETS, TESTNET_MARKETS } from 'sdk/constants/futures'; import { SECONDS_PER_DAY } from 'sdk/constants/period'; -import { FundingRateUpdate, FuturesMarketAsset, MarketClosureReason } from 'sdk/types/futures'; +import { + FundingRateUpdate, + FuturesMarketAsset, + FuturesMarketKey, + FuturesPosition, + FuturesPotentialTradeDetails, + FuturesVolumes, + MarketClosureReason, + PositionDetail, + PositionSide, + PostTradeDetailsResponse, + PotentialTradeStatus, +} from 'sdk/types/futures'; +import { + CrossMarginOrderType, + CrossMarginSettings, + IsolatedMarginOrderType, +} from 'state/futures/types'; +import { zeroBN } from 'utils/formatters/number'; import logError from 'utils/logError'; export const getFuturesEndpoint = (networkId: number): string => { @@ -100,3 +120,167 @@ export const getReasonFromCode = ( return 'unknown'; } }; + +export const calculateVolumes = ( + futuresHourlyStats: FuturesAggregateStatResult[] +): FuturesVolumes => { + const volumes: FuturesVolumes = futuresHourlyStats.reduce( + (acc: FuturesVolumes, { asset, volume, trades }) => { + return { + ...acc, + [asset]: { + volume: volume.div(ETH_UNIT).add(acc[asset]?.volume ?? 0), + trades: trades.add(acc[asset]?.trades ?? 0), + }, + }; + }, + {} + ); + return volumes; +}; + +export const mapFuturesPosition = ( + positionDetail: PositionDetail, + canLiquidatePosition: boolean, + asset: FuturesMarketAsset, + marketKey: FuturesMarketKey +): FuturesPosition => { + const { + remainingMargin, + accessibleMargin, + position: { fundingIndex, lastPrice, size, margin }, + accruedFunding, + notionalValue, + liquidationPrice, + profitLoss, + } = positionDetail; + const initialMargin = wei(margin); + const pnl = wei(profitLoss).add(wei(accruedFunding)); + const pnlPct = initialMargin.gt(0) ? pnl.div(wei(initialMargin)) : wei(0); + + return { + asset, + marketKey, + remainingMargin: wei(remainingMargin), + accessibleMargin: wei(accessibleMargin), + position: wei(size).eq(zeroBN) + ? null + : { + canLiquidatePosition: !!canLiquidatePosition, + side: wei(size).gt(zeroBN) ? PositionSide.LONG : PositionSide.SHORT, + notionalValue: wei(notionalValue).abs(), + accruedFunding: wei(accruedFunding), + initialMargin, + profitLoss: wei(profitLoss), + fundingIndex: Number(fundingIndex), + lastPrice: wei(lastPrice), + size: wei(size).abs(), + liquidationPrice: wei(liquidationPrice), + initialLeverage: initialMargin.gt(0) + ? wei(size).mul(wei(lastPrice)).div(initialMargin).abs() + : wei(0), + pnl, + pnlPct, + marginRatio: wei(notionalValue).eq(zeroBN) + ? zeroBN + : wei(remainingMargin).div(wei(notionalValue).abs()), + leverage: wei(remainingMargin).eq(zeroBN) + ? zeroBN + : wei(notionalValue).div(wei(remainingMargin)).abs(), + }, + }; +}; + +export const serializePotentialTrade = ( + preview: FuturesPotentialTradeDetails +): FuturesPotentialTradeDetails => ({ + ...preview, + size: preview.size.toString(), + sizeDelta: preview.sizeDelta.toString(), + liqPrice: preview.liqPrice.toString(), + margin: preview.margin.toString(), + price: preview.price.toString(), + fee: preview.fee.toString(), + leverage: preview.leverage.toString(), + notionalValue: preview.notionalValue.toString(), +}); + +export const unserializePotentialTrade = ( + preview: FuturesPotentialTradeDetails +): FuturesPotentialTradeDetails => ({ + ...preview, + size: wei(preview.size), + sizeDelta: wei(preview.sizeDelta), + liqPrice: wei(preview.liqPrice), + margin: wei(preview.margin), + price: wei(preview.price), + fee: wei(preview.fee), + leverage: wei(preview.leverage), + notionalValue: wei(preview.notionalValue), +}); + +export const formatPotentialTrade = ( + preview: PostTradeDetailsResponse, + nativeSizeDelta: Wei, + leverageSide: PositionSide +) => { + const { fee, liqPrice, margin, price, size, status } = preview; + + return { + fee: wei(fee), + liqPrice: wei(liqPrice), + margin: wei(margin), + price: wei(price), + size: wei(size), + sizeDelta: nativeSizeDelta, + side: leverageSide, + leverage: wei(margin).eq(0) ? wei(0) : wei(size).mul(wei(price)).div(wei(margin)).abs(), + notionalValue: wei(size).mul(wei(price)), + status, + showStatus: status > 0, // 0 is success + statusMessage: getTradeStatusMessage(status), + }; +}; + +const SUCCESS = 'Success'; +const UNKNOWN = 'Unknown'; + +export const getTradeStatusMessage = (status: PotentialTradeStatus): string => { + if (typeof status !== 'number') { + return UNKNOWN; + } + + if (status === 0) { + return SUCCESS; + } else if (PotentialTradeStatus[status]) { + return POTENTIAL_TRADE_STATUS_TO_MESSAGE[PotentialTradeStatus[status]]; + } else { + return UNKNOWN; + } +}; + +// https://github.com/Synthetixio/synthetix/blob/4d2add4f74c68ac4f1106f6e7be4c31d4f1ccc76/contracts/PerpsV2MarketBase.sol#L130-L141 +export const POTENTIAL_TRADE_STATUS_TO_MESSAGE: { [key: string]: string } = { + INVALID_PRICE: 'Invalid price', + PRICE_OUT_OF_BOUNDS: 'Price out of acceptable range', + CAN_LIQUIDATE: 'Position can be liquidated', + CANNOT_LIQUIDATE: 'Position cannot be liquidated', + MAX_MARKET_SIZE_EXCEEDED: 'Max market size exceeded', + MAX_LEVERAGE_EXCEEDED: 'Max leverage exceeded', + INSUFFICIENT_MARGIN: 'Insufficient margin', + NOT_PERMITTED: 'Not permitted by this address', + NIL_ORDER: 'Cannot submit empty order', + NO_POSITION_OPEN: 'No position open', + PRICE_TOO_VOLATILE: 'Price too volatile', +}; + +export const calculateCrossMarginFee = ( + orderType: CrossMarginOrderType | IsolatedMarginOrderType, + susdSize: Wei, + feeRates: CrossMarginSettings +) => { + if (orderType !== 'limit' && orderType !== 'stop market') return zeroBN; + const advancedOrderFeeRate = + orderType === 'limit' ? feeRates.limitOrderFee : feeRates.stopOrderFee; + return susdSize.mul(advancedOrderFeeRate); +}; diff --git a/sections/dashboard/FuturesHistoryTable/FuturesHistoryTable.tsx b/sections/dashboard/FuturesHistoryTable/FuturesHistoryTable.tsx index e0b1e3c080..89ee48a5c0 100644 --- a/sections/dashboard/FuturesHistoryTable/FuturesHistoryTable.tsx +++ b/sections/dashboard/FuturesHistoryTable/FuturesHistoryTable.tsx @@ -5,7 +5,6 @@ import Link from 'next/link'; import { FC, useMemo, ReactElement, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CellProps } from 'react-table'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import Currency from 'components/Currency'; @@ -25,7 +24,8 @@ import { FuturesTrade } from 'queries/futures/types'; import useGetAllFuturesTradesForAccount from 'queries/futures/useGetAllFuturesTradesForAccount'; import TradeDrawer from 'sections/futures/MobileTrade/drawers/TradeDrawer'; import { TradeStatus } from 'sections/futures/types'; -import { futuresAccountTypeState } from 'store/futures'; +import { selectFuturesType } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { formatShortDateWithoutYear } from 'utils/formatters/date'; import { formatCryptoCurrency, formatDollars } from 'utils/formatters/number'; import { @@ -40,7 +40,7 @@ import TimeDisplay from '../../futures/Trades/TimeDisplay'; const FuturesHistoryTable: FC = () => { const [selectedTrade, setSelectedTrade] = useState(); - const accountType = useRecoilValue(futuresAccountTypeState); + const accountType = useAppSelector(selectFuturesType); const { walletAddress } = Connector.useContainer(); const { t } = useTranslation(); diff --git a/sections/dashboard/FuturesMarketsTable/FuturesMarketsTable.tsx b/sections/dashboard/FuturesMarketsTable/FuturesMarketsTable.tsx index 5decf25275..393ee36e83 100644 --- a/sections/dashboard/FuturesMarketsTable/FuturesMarketsTable.tsx +++ b/sections/dashboard/FuturesMarketsTable/FuturesMarketsTable.tsx @@ -14,15 +14,15 @@ import Table from 'components/Table'; import { DEFAULT_CRYPTO_DECIMALS } from 'constants/defaults'; import ROUTES from 'constants/routes'; import Connector from 'containers/Connector'; -import { FundingRateResponse } from 'queries/futures/useGetAverageFundingRateForMarkets'; -import { selectMarkets } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { FundingRateResponse } from 'sdk/types/futures'; import { - pastRatesState, - fundingRatesState, - futuresVolumesState, - futuresAccountTypeState, -} from 'store/futures'; + selectAverageFundingRates, + selectFuturesType, + selectMarkets, + selectMarketVolumes, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; +import { pastRatesState } from 'store/futures'; import { getSynthDescription, MarketKeyByAsset, FuturesMarketAsset } from 'utils/futures'; const FuturesMarketsTable: FC = () => { @@ -31,10 +31,10 @@ const FuturesMarketsTable: FC = () => { const { synthsMap } = Connector.useContainer(); const futuresMarkets = useAppSelector(selectMarkets); - const fundingRates = useRecoilValue(fundingRatesState); + const fundingRates = useAppSelector(selectAverageFundingRates); const pastRates = useRecoilValue(pastRatesState); - const futuresVolumes = useRecoilValue(futuresVolumesState); - const accountType = useRecoilValue(futuresAccountTypeState); + const futuresVolumes = useAppSelector(selectMarketVolumes); + const accountType = useAppSelector(selectFuturesType); let data = useMemo(() => { return futuresMarkets.map((market) => { diff --git a/sections/dashboard/FuturesPositionsTable/FuturesPositionsTable.tsx b/sections/dashboard/FuturesPositionsTable/FuturesPositionsTable.tsx index c1b897b3f8..912492590f 100644 --- a/sections/dashboard/FuturesPositionsTable/FuturesPositionsTable.tsx +++ b/sections/dashboard/FuturesPositionsTable/FuturesPositionsTable.tsx @@ -19,9 +19,14 @@ import Connector from 'containers/Connector'; import useIsL2 from 'hooks/useIsL2'; import useNetworkSwitcher from 'hooks/useNetworkSwitcher'; import { FuturesAccountType } from 'queries/futures/subgraph'; -import { selectMarketAsset, selectMarkets } from 'state/futures/selectors'; +import { + selectCrossMarginPositions, + selectIsolatedMarginPositions, + selectMarketAsset, + selectMarkets, +} from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { positionsState, positionHistoryState } from 'store/futures'; +import { positionHistoryState } from 'store/futures'; import { formatNumber } from 'utils/formatters/number'; import { getSynthDescription } from 'utils/futures'; @@ -43,13 +48,15 @@ const FuturesPositionsTable: FC = ({ const isL2 = useIsL2(); - const positions = useRecoilValue(positionsState); + const isolatedPositions = useAppSelector(selectIsolatedMarginPositions); + const crossMarginPositions = useAppSelector(selectCrossMarginPositions); const positionHistory = useRecoilValue(positionHistoryState); const currentMarket = useAppSelector(selectMarketAsset); const futuresMarkets = useAppSelector(selectMarkets); let data = useMemo(() => { - return positions[accountType] + const positions = accountType === 'cross_margin' ? crossMarginPositions : isolatedPositions; + return positions .map((position) => { const market = futuresMarkets.find((market) => market.asset === position.asset); const description = getSynthDescription(position.asset, synthsMap, t); @@ -69,7 +76,8 @@ const FuturesPositionsTable: FC = ({ position.position && (position?.market?.asset !== currentMarket || showCurrentMarket) ); }, [ - positions, + isolatedPositions, + crossMarginPositions, accountType, futuresMarkets, positionHistory, diff --git a/sections/dashboard/MobileDashboard/FuturesMarkets.tsx b/sections/dashboard/MobileDashboard/FuturesMarkets.tsx index 6583949d73..d63dceb22f 100644 --- a/sections/dashboard/MobileDashboard/FuturesMarkets.tsx +++ b/sections/dashboard/MobileDashboard/FuturesMarkets.tsx @@ -1,12 +1,10 @@ import { wei } from '@synthetixio/wei'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import { SectionHeader, SectionTitle } from 'sections/futures/MobileTrade/common'; -import { selectMarkets } from 'state/futures/selectors'; +import { selectMarkets, selectMarketVolumes } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { futuresVolumesState } from 'store/futures'; import { formatDollars, formatNumber, zeroBN } from 'utils/formatters/number'; import FuturesMarketsTable from '../FuturesMarketsTable'; @@ -16,7 +14,7 @@ const FuturesMarkets = () => { const { t } = useTranslation(); const futuresMarkets = useAppSelector(selectMarkets); - const futuresVolumes = useRecoilValue(futuresVolumesState); + const futuresVolumes = useAppSelector(selectMarketVolumes); const openInterest = useMemo(() => { return ( diff --git a/sections/dashboard/MobileDashboard/OpenPositions.tsx b/sections/dashboard/MobileDashboard/OpenPositions.tsx index 81b6622a0f..1100176b95 100644 --- a/sections/dashboard/MobileDashboard/OpenPositions.tsx +++ b/sections/dashboard/MobileDashboard/OpenPositions.tsx @@ -1,14 +1,20 @@ import Wei from '@synthetixio/wei'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { SetterOrUpdater, useRecoilValue } from 'recoil'; +import { SetterOrUpdater } from 'recoil'; import styled from 'styled-components'; import TabButton from 'components/Button/TabButton'; import { TabPanel } from 'components/Tab'; import { FuturesAccountTypes } from 'queries/futures/types'; import { SectionHeader, SectionTitle } from 'sections/futures/MobileTrade/common'; -import { balancesState, portfolioState, positionsState } from 'store/futures'; +import { selectBalances } from 'state/balances/selectors'; +import { + selectCrossMarginPositions, + selectFuturesPortfolio, + selectIsolatedMarginPositions, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { formatDollars } from 'utils/formatters/number'; import FuturesPositionsTable from '../FuturesPositionsTable'; @@ -37,16 +43,17 @@ const OpenPositions: React.FC = ({ exchangeTokenBalances, }) => { const { t } = useTranslation(); - const positions = useRecoilValue(positionsState); - const portfolio = useRecoilValue(portfolioState); - const balances = useRecoilValue(balancesState); + const crossPositions = useAppSelector(selectCrossMarginPositions); + const isolatedPositions = useAppSelector(selectIsolatedMarginPositions); + const portfolio = useAppSelector(selectFuturesPortfolio); + const balances = useAppSelector(selectBalances); const POSITIONS_TABS = useMemo( () => [ { name: PositionsTab.CROSS_MARGIN, label: t('dashboard.overview.positions-tabs.cross-margin'), - badge: positions[FuturesAccountTypes.CROSS_MARGIN].length, + badge: crossPositions.length, active: activePositionsTab === PositionsTab.CROSS_MARGIN, detail: formatDollars(portfolio.crossMarginFutures), disabled: false, @@ -55,7 +62,7 @@ const OpenPositions: React.FC = ({ { name: PositionsTab.ISOLATED_MARGIN, label: t('dashboard.overview.positions-tabs.isolated-margin'), - badge: positions[FuturesAccountTypes.ISOLATED_MARGIN].length, + badge: isolatedPositions.length, active: activePositionsTab === PositionsTab.ISOLATED_MARGIN, detail: formatDollars(portfolio.isolatedMarginFutures), disabled: false, @@ -72,7 +79,8 @@ const OpenPositions: React.FC = ({ ], [ t, - positions, + isolatedPositions, + crossPositions, activePositionsTab, portfolio.crossMarginFutures, portfolio.isolatedMarginFutures, diff --git a/sections/dashboard/Overview/Overview.tsx b/sections/dashboard/Overview/Overview.tsx index a321a65aba..07afc30ea9 100644 --- a/sections/dashboard/Overview/Overview.tsx +++ b/sections/dashboard/Overview/Overview.tsx @@ -1,7 +1,7 @@ import Wei from '@synthetixio/wei'; import { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilState } from 'recoil'; import styled from 'styled-components'; import { erc20ABI, useContractRead } from 'wagmi'; @@ -15,9 +15,14 @@ import Connector from 'containers/Connector'; import useIsL2 from 'hooks/useIsL2'; import { FuturesAccountTypes } from 'queries/futures/types'; import { CompetitionBanner } from 'sections/shared/components/CompetitionBanner'; +import { selectBalances } from 'state/balances/selectors'; import { sdk } from 'state/config'; +import { + selectActiveCrossPositionsCount, + selectActiveIsolatedPositionsCount, + selectFuturesPortfolio, +} from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { balancesState, portfolioState, positionsState } from 'store/futures'; import { activePositionsTabState } from 'store/ui'; import { formatDollars, toWei, weiFromWei, zeroBN } from 'utils/formatters/number'; import logError from 'utils/logError'; @@ -41,9 +46,10 @@ export enum PositionsTab { const Overview: FC = () => { const { t } = useTranslation(); - const balances = useRecoilValue(balancesState); - const portfolio = useRecoilValue(portfolioState); - const positions = useRecoilValue(positionsState); + const balances = useAppSelector(selectBalances); + const portfolio = useAppSelector(selectFuturesPortfolio); + const isolatedPositionsCount = useAppSelector(selectActiveIsolatedPositionsCount); + const crossPositionsCount = useAppSelector(selectActiveCrossPositionsCount); const [activePositionsTab, setActivePositionsTab] = useRecoilState( activePositionsTabState @@ -163,8 +169,6 @@ const Overview: FC = () => { }, [kwentaBalance, noKwentaFound, oneInchEnabled, synthsMap, tokenBalances]); const POSITIONS_TABS = useMemo(() => { - const crossPositions = positions.cross_margin.filter(({ position }) => !!position).length; - const isolatedPositions = positions.isolated_margin.filter(({ position }) => !!position).length; const exchangeTokenBalances = exchangeTokens.reduce( (initial: Wei, { usdBalance }: { usdBalance: Wei }) => initial.add(usdBalance), zeroBN @@ -173,7 +177,7 @@ const Overview: FC = () => { { name: PositionsTab.CROSS_MARGIN, label: t('dashboard.overview.positions-tabs.cross-margin'), - badge: crossPositions, + badge: crossPositionsCount, titleIcon: , active: activePositionsTab === PositionsTab.CROSS_MARGIN, detail: formatDollars(portfolio.crossMarginFutures), @@ -183,7 +187,7 @@ const Overview: FC = () => { { name: PositionsTab.ISOLATED_MARGIN, label: t('dashboard.overview.positions-tabs.isolated-margin'), - badge: isolatedPositions, + badge: isolatedPositionsCount, active: activePositionsTab === PositionsTab.ISOLATED_MARGIN, titleIcon: , detail: formatDollars(portfolio.isolatedMarginFutures), @@ -200,8 +204,8 @@ const Overview: FC = () => { }, ]; }, [ - positions.cross_margin, - positions.isolated_margin, + crossPositionsCount, + isolatedPositionsCount, exchangeTokens, balances.totalUSDBalance, t, diff --git a/sections/dashboard/PortfolioChart/PortfolioChart.tsx b/sections/dashboard/PortfolioChart/PortfolioChart.tsx index 6c5553da3f..1e8f7fe847 100644 --- a/sections/dashboard/PortfolioChart/PortfolioChart.tsx +++ b/sections/dashboard/PortfolioChart/PortfolioChart.tsx @@ -1,19 +1,20 @@ import Wei from '@synthetixio/wei'; import { FC, useMemo } from 'react'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import Currency from 'components/Currency'; import { MobileHiddenView, MobileOnlyView } from 'components/Media'; -import { balancesState, portfolioState } from 'store/futures'; +import { selectBalances } from 'state/balances/selectors'; +import { selectFuturesPortfolio } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; type PortfolioChartProps = { exchangeTokenBalances: Wei; }; const PortfolioChart: FC = ({ exchangeTokenBalances }) => { - const portfolio = useRecoilValue(portfolioState); - const balances = useRecoilValue(balancesState); + const portfolio = useAppSelector(selectFuturesPortfolio); + const balances = useAppSelector(selectBalances); const total = useMemo( () => portfolio.total.add(balances.totalUSDBalance).add(exchangeTokenBalances), diff --git a/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx b/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx index cb53b6602c..c30ebbab40 100644 --- a/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx +++ b/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx @@ -15,9 +15,10 @@ import Table, { TableNoResults } from 'components/Table'; import { NO_VALUE } from 'constants/placeholder'; import Connector from 'containers/Connector'; import { Price } from 'queries/rates/types'; +import { selectBalances } from 'state/balances/selectors'; import { selectExchangeRates } from 'state/exchange/selectors'; import { useAppSelector } from 'state/hooks'; -import { balancesState, pastRatesState } from 'store/futures'; +import { pastRatesState } from 'store/futures'; import { sortWei } from 'utils/balances'; import { formatNumber, zeroBN } from 'utils/formatters/number'; import { isDecimalFour } from 'utils/futures'; @@ -60,10 +61,10 @@ const SynthBalancesTable: FC = ({ exchangeTokens }) => const { synthsMap } = Connector.useContainer(); const pastRates = useRecoilValue(pastRatesState); const exchangeRates = useAppSelector(selectExchangeRates); - const { balances } = useRecoilValue(balancesState); + const { synthBalances } = useAppSelector(selectBalances); const synthTokens = useMemo(() => { - return balances.map((synthBalance: SynthBalance) => { + return synthBalances.map((synthBalance: SynthBalance) => { const { currencyKey, balance, usdBalance } = synthBalance; const price = exchangeRates && exchangeRates[currencyKey]; @@ -79,7 +80,7 @@ const SynthBalancesTable: FC = ({ exchangeTokens }) => priceChange: calculatePriceChange(price, pastPrice), }; }); - }, [pastRates, exchangeRates, balances, synthsMap]); + }, [pastRates, exchangeRates, synthBalances, synthsMap]); const data = [...exchangeTokens, ...synthTokens].sort((a, b) => sortWei(a.usdBalance, b.usdBalance, 'descending') diff --git a/sections/futures/CrossMarginOnboard/CrossMarginOnboard.tsx b/sections/futures/CrossMarginOnboard/CrossMarginOnboard.tsx index 6350c923a0..1081c537da 100644 --- a/sections/futures/CrossMarginOnboard/CrossMarginOnboard.tsx +++ b/sections/futures/CrossMarginOnboard/CrossMarginOnboard.tsx @@ -4,7 +4,7 @@ import { constants } from 'ethers'; import { defaultAbiCoder } from 'ethers/lib/utils'; import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilState } from 'recoil'; import styled from 'styled-components'; import CompleteCheck from 'assets/svg/futures/onboard-complete-check.svg'; @@ -26,7 +26,9 @@ import { FuturesAccountState } from 'queries/futures/types'; import useQueryCrossMarginAccount, { useStoredCrossMarginAccounts, } from 'queries/futures/useQueryCrossMarginAccount'; -import { balancesState, futuresAccountState } from 'store/futures'; +import { selectBalances } from 'state/balances/selectors'; +import { useAppSelector } from 'state/hooks'; +import { futuresAccountState } from 'store/futures'; import { FlexDivRowCentered } from 'styles/common'; import { isUserDeniedError } from 'utils/formatters/error'; import { zeroBN } from 'utils/formatters/number'; @@ -59,7 +61,7 @@ export default function CrossMarginOnboard({ onClose, isOpen }: Props) { const queryCrossMarginAccount = useQueryCrossMarginAccount(); const { storeCrossMarginAccount } = useStoredCrossMarginAccounts(); const [futuresAccount, setFuturesAccount] = useRecoilState(futuresAccountState); - const balances = useRecoilValue(balancesState); + const balances = useAppSelector(selectBalances); const [depositAmount, setDepositAmount] = useState(''); const [depositComplete, setDepositComplete] = useState(false); diff --git a/sections/futures/FeeInfoBox/FeeInfoBox.tsx b/sections/futures/FeeInfoBox/FeeInfoBox.tsx index c2b05ba1eb..d1471ad12b 100644 --- a/sections/futures/FeeInfoBox/FeeInfoBox.tsx +++ b/sections/futures/FeeInfoBox/FeeInfoBox.tsx @@ -1,39 +1,40 @@ import React, { FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import TimerIcon from 'assets/svg/app/timer.svg'; import InfoBox, { DetailedInfo } from 'components/InfoBox/InfoBox'; import StyledTooltip from 'components/Tooltip/StyledTooltip'; import { NO_VALUE } from 'constants/placeholder'; -import { selectMarketInfo } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; import { - tradeFeesState, - orderTypeState, - sizeDeltaState, - futuresAccountTypeState, - crossMarginSettingsState, - dynamicFeeRateState, -} from 'store/futures'; + selectCrossMarginSettings, + selectCrossMarginTradeFees, + selectDynamicFeeRate, + selectFuturesType, + selectIsolatedMarginFee, + selectMarketInfo, + selectOrderType, + selectTradeSizeInputs, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { computeNPFee, computeMarketFee } from 'utils/costCalculations'; import { formatCurrency, formatDollars, formatPercent, zeroBN } from 'utils/formatters/number'; const FeeInfoBox: React.FC = () => { - const orderType = useRecoilValue(orderTypeState); - const fees = useRecoilValue(tradeFeesState); - const dynamicFeeRate = useRecoilValue(dynamicFeeRateState); - const sizeDelta = useRecoilValue(sizeDeltaState); - const accountType = useRecoilValue(futuresAccountTypeState); - const { tradeFee: crossMarginTradeFee, limitOrderFee, stopOrderFee } = useRecoilValue( - crossMarginSettingsState + const orderType = useAppSelector(selectOrderType); + const crossMarginFees = useAppSelector(selectCrossMarginTradeFees); + const isolatedMarginFee = useAppSelector(selectIsolatedMarginFee); + const dynamicFeeRate = useAppSelector(selectDynamicFeeRate); + const { nativeSizeDelta } = useAppSelector(selectTradeSizeInputs); + const accountType = useAppSelector(selectFuturesType); + const { tradeFee: crossMarginTradeFeeRate, limitOrderFee, stopOrderFee } = useAppSelector( + selectCrossMarginSettings ); const marketInfo = useAppSelector(selectMarketInfo); - const { commitDeposit, nextPriceFee } = useMemo(() => computeNPFee(marketInfo, sizeDelta), [ + const { commitDeposit, nextPriceFee } = useMemo(() => computeNPFee(marketInfo, nativeSizeDelta), [ marketInfo, - sizeDelta, + nativeSizeDelta, ]); const totalDeposit = useMemo(() => { @@ -44,9 +45,9 @@ const FeeInfoBox: React.FC = () => { return (nextPriceFee ?? zeroBN).sub(commitDeposit ?? zeroBN); }, [commitDeposit, nextPriceFee]); - const staticRate = useMemo(() => computeMarketFee(marketInfo, sizeDelta), [ + const staticRate = useMemo(() => computeMarketFee(marketInfo, nativeSizeDelta), [ marketInfo, - sizeDelta, + nativeSizeDelta, ]); const orderFeeRate = useMemo( @@ -75,31 +76,31 @@ const FeeInfoBox: React.FC = () => { const feesInfo = useMemo>(() => { const crossMarginFeeInfo = { 'Protocol Fee': { - value: formatDollars(fees.staticFee, { - minDecimals: fees.staticFee.lt(0.01) ? 4 : 2, + value: formatDollars(crossMarginFees.staticFee, { + minDecimals: crossMarginFees.staticFee.lt(0.01) ? 4 : 2, }), keyNode: marketCostTooltip, }, 'Limit / Stop Fee': - fees.limitStopOrderFee.gt(0) && orderFeeRate + crossMarginFees.limitStopOrderFee.gt(0) && orderFeeRate ? { - value: formatDollars(fees.limitStopOrderFee, { - minDecimals: fees.limitStopOrderFee.lt(0.01) ? 4 : 2, + value: formatDollars(crossMarginFees.limitStopOrderFee, { + minDecimals: crossMarginFees.limitStopOrderFee.lt(0.01) ? 4 : 2, }), keyNode: formatPercent(orderFeeRate), } : null, 'Cross Margin Fee': { - value: formatDollars(fees.crossMarginFee, { - minDecimals: fees.crossMarginFee.lt(0.01) ? 4 : 2, + value: formatDollars(crossMarginFees.crossMarginFee, { + minDecimals: crossMarginFees.crossMarginFee.lt(0.01) ? 4 : 2, }), spaceBeneath: true, - keyNode: formatPercent(crossMarginTradeFee), + keyNode: formatPercent(crossMarginTradeFeeRate), }, 'Total Fee': { - value: formatDollars(fees.total, { - minDecimals: fees.total.lt(0.01) ? 4 : 2, + value: formatDollars(crossMarginFees.total, { + minDecimals: crossMarginFees.total.lt(0.01) ? 4 : 2, }), }, }; @@ -108,7 +109,7 @@ const FeeInfoBox: React.FC = () => { ...crossMarginFeeInfo, 'Keeper Deposit': { value: !!marketInfo?.keeperDeposit - ? formatCurrency('ETH', fees.keeperEthDeposit, { currencyKey: 'ETH' }) + ? formatCurrency('ETH', crossMarginFees.keeperEthDeposit, { currencyKey: 'ETH' }) : NO_VALUE, }, }; @@ -133,15 +134,15 @@ const FeeInfoBox: React.FC = () => { }, 'Estimated Fees': { value: formatDollars(totalDeposit.add(nextPriceDiscount ?? zeroBN)), - keyNode: fees.dynamicFeeRate?.gt(0) ? : null, + keyNode: dynamicFeeRate?.gt(0) ? : null, }, }; } return accountType === 'isolated_margin' ? { Fee: { - value: formatDollars(fees.total, { - minDecimals: fees.total.lt(0.01) ? 4 : 2, + value: formatDollars(isolatedMarginFee, { + minDecimals: isolatedMarginFee.lt(0.01) ? 4 : 2, }), keyNode: marketCostTooltip, }, @@ -149,9 +150,11 @@ const FeeInfoBox: React.FC = () => { : crossMarginFeeInfo; }, [ orderType, - crossMarginTradeFee, - fees, + crossMarginTradeFeeRate, + isolatedMarginFee, + crossMarginFees, orderFeeRate, + dynamicFeeRate, commitDeposit, accountType, marketInfo?.keeperDeposit, diff --git a/sections/futures/LeverageInput/LeverageInput.tsx b/sections/futures/LeverageInput/LeverageInput.tsx index 710f6b7def..1571771e26 100644 --- a/sections/futures/LeverageInput/LeverageInput.tsx +++ b/sections/futures/LeverageInput/LeverageInput.tsx @@ -1,39 +1,56 @@ -import { FC, useMemo, useState } from 'react'; +import { wei } from '@synthetixio/wei'; +import { FC, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useSetRecoilState, useRecoilValue } from 'recoil'; import styled from 'styled-components'; import Button from 'components/Button'; import CustomNumericInput from 'components/Input/CustomNumericInput'; import { DEFAULT_FIAT_DECIMALS } from 'constants/defaults'; -import { useFuturesContext } from 'contexts/FuturesContext'; -import { selectMarketInfo, selectMaxLeverage } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { editIsolatedMarginSize } from 'state/futures/actions'; +import { setIsolatedMarginLeverageInput } from 'state/futures/reducer'; import { - leverageValueCommittedState, - nextPriceDisclaimerState, - orderTypeState, - positionState, - futuresTradeInputsState, -} from 'store/futures'; + selectIsolatedLeverageInput, + selectIsolatedMarginLeverage, + selectMarketAssetRate, + selectMarketInfo, + selectMaxLeverage, + selectNextPriceDisclaimer, + selectOrderType, + selectPosition, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { FlexDivCol, FlexDivRow } from 'styles/common'; -import { truncateNumbers } from 'utils/formatters/number'; +import { floorNumber, truncateNumbers, zeroBN } from 'utils/formatters/number'; import LeverageSlider from '../LeverageSlider'; const LeverageInput: FC = () => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); const [mode, setMode] = useState<'slider' | 'input'>('input'); - const { leverage } = useRecoilValue(futuresTradeInputsState); - const orderType = useRecoilValue(orderTypeState); - const isDisclaimerDisplayed = useRecoilValue(nextPriceDisclaimerState); - const setIsLeverageValueCommitted = useSetRecoilState(leverageValueCommittedState); - const position = useRecoilValue(positionState); - + const leverage = useAppSelector(selectIsolatedMarginLeverage); + const orderType = useAppSelector(selectOrderType); + const isDisclaimerDisplayed = useAppSelector(selectNextPriceDisclaimer); + const position = useAppSelector(selectPosition); const marketInfo = useAppSelector(selectMarketInfo); const maxLeverage = useAppSelector(selectMaxLeverage); - - const { onLeverageChange } = useFuturesContext(); + const marketAssetRate = useAppSelector(selectMarketAssetRate); + const leverageInput = useAppSelector(selectIsolatedLeverageInput); + + const onLeverageChange = useCallback( + (newLeverage: number) => { + const remainingMargin = position?.remainingMargin ?? zeroBN; + const newTradeSize = + marketAssetRate.eq(0) || remainingMargin.eq(0) + ? '' + : wei(newLeverage).mul(remainingMargin).div(marketAssetRate).toString(); + const input = truncateNumbers(newLeverage, DEFAULT_FIAT_DECIMALS); + dispatch(setIsolatedMarginLeverageInput(input)); + const floored = floorNumber(Number(newTradeSize), 4); + dispatch(editIsolatedMarginSize(String(floored), 'native')); + }, + [position?.remainingMargin, marketAssetRate, dispatch] + ); const modeButton = useMemo(() => { return ( @@ -79,22 +96,19 @@ const LeverageInput: FC = () => { maxValue={Number(truncateMaxLeverage)} value={Number(truncateLeverage)} onChange={(_, newValue) => { - setIsLeverageValueCommitted(false); onLeverageChange(newValue as number); }} - onChangeCommitted={() => setIsLeverageValueCommitted(true)} /> ) : ( { - setIsLeverageValueCommitted(true); onLeverageChange(Number(newValue)); }} disabled={isDisabled} diff --git a/sections/futures/MarketDetails/useGetMarketData.ts b/sections/futures/MarketDetails/useGetMarketData.ts index f022b254fd..637984e29b 100644 --- a/sections/futures/MarketDetails/useGetMarketData.ts +++ b/sections/futures/MarketDetails/useGetMarketData.ts @@ -12,9 +12,10 @@ import { selectMarketAsset, selectMarketInfo, selectMarketKey, + selectMarketVolumes, } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { futuresVolumesState, pastRatesState } from 'store/futures'; +import { pastRatesState } from 'store/futures'; import { isFiatCurrency } from 'utils/currencies'; import { formatCurrency, formatPercent, zeroBN } from 'utils/formatters/number'; import { isDecimalFour } from 'utils/futures'; @@ -32,7 +33,7 @@ const useGetMarketData = (mobile?: boolean) => { const marketInfo = useAppSelector(selectMarketInfo); const pastRates = useRecoilValue(pastRatesState); - const futuresVolumes = useRecoilValue(futuresVolumesState); + const futuresVolumes = useAppSelector(selectMarketVolumes); const { selectedPriceCurrency } = useSelectedPriceCurrency(); diff --git a/sections/futures/MarketInfoBox/MarketInfoBox.tsx b/sections/futures/MarketInfoBox/MarketInfoBox.tsx index 120dc11e1a..3ffba5630e 100644 --- a/sections/futures/MarketInfoBox/MarketInfoBox.tsx +++ b/sections/futures/MarketInfoBox/MarketInfoBox.tsx @@ -1,33 +1,31 @@ import Wei, { wei } from '@synthetixio/wei'; -import React, { useMemo } from 'react'; -import { useRecoilValue } from 'recoil'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import InfoBox from 'components/InfoBox'; import PreviewArrow from 'components/PreviewArrow'; -import { FuturesPotentialTradeDetails } from 'queries/futures/types'; -import { selectMarketInfo, selectMaxLeverage } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { FuturesPotentialTradeDetails } from 'sdk/types/futures'; import { - leverageSideState, - orderTypeState, - positionState, - potentialTradeDetailsState, - futuresTradeInputsState, -} from 'store/futures'; + selectLeverageSide, + selectMarketInfo, + selectMaxLeverage, + selectOrderType, + selectPosition, + selectTradePreview, + selectTradeSizeInputs, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { computeNPFee } from 'utils/costCalculations'; import { formatDollars, formatPercent, zeroBN } from 'utils/formatters/number'; -import { PositionSide } from '../types'; - const MarketInfoBox: React.FC = () => { - const position = useRecoilValue(positionState); - const orderType = useRecoilValue(orderTypeState); - const leverageSide = useRecoilValue(leverageSideState); - const { nativeSize } = useRecoilValue(futuresTradeInputsState); - const potentialTrade = useRecoilValue(potentialTradeDetailsState); + const orderType = useAppSelector(selectOrderType); + const leverageSide = useAppSelector(selectLeverageSide); + const { nativeSize, nativeSizeDelta } = useAppSelector(selectTradeSizeInputs); + const potentialTrade = useAppSelector(selectTradePreview); const marketInfo = useAppSelector(selectMarketInfo); + const position = useAppSelector(selectPosition); const maxLeverage = useAppSelector(selectMaxLeverage); const totalMargin = position?.remainingMargin ?? zeroBN; @@ -39,13 +37,18 @@ const MarketInfoBox: React.FC = () => { ? totalMargin.sub(availableMargin).div(totalMargin) : zeroBN; + const minInitialMargin = useMemo(() => marketInfo?.minInitialMargin ?? zeroBN, [ + marketInfo?.minInitialMargin, + ]); + const isNextPriceOrder = orderType === 'next price'; const positionSize = position?.position?.size ? wei(position?.position?.size) : zeroBN; const orderDetails = useMemo(() => { - const newSize = - leverageSide === PositionSide.LONG ? wei(nativeSize || 0) : wei(nativeSize || 0).neg(); - return { newSize, size: (positionSize ?? zeroBN).add(newSize).abs() }; + return { + newSize: nativeSize, + size: (positionSize ?? zeroBN).add(nativeSizeDelta).abs(), + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [leverageSide, positionSize]); @@ -58,44 +61,50 @@ const MarketInfoBox: React.FC = () => { return (commitDeposit ?? zeroBN).add(marketInfo?.keeperDeposit ?? zeroBN); }, [commitDeposit, marketInfo?.keeperDeposit]); - const getPotentialAvailableMargin = ( - trade: FuturesPotentialTradeDetails | null, - marketMaxLeverage: Wei | undefined - ) => { - let inaccessible; + const getPotentialAvailableMargin = useCallback( + (trade: FuturesPotentialTradeDetails | null, marketMaxLeverage: Wei | undefined) => { + let inaccessible; - inaccessible = - (marketMaxLeverage && trade?.notionalValue.div(marketMaxLeverage).abs()) ?? zeroBN; + inaccessible = + (marketMaxLeverage && trade?.notionalValue.div(marketMaxLeverage).abs()) ?? zeroBN; - // If the user has a position open, we'll enforce a min initial margin requirement. - if (inaccessible.gt(0)) { - if (inaccessible.lt(trade?.minInitialMargin ?? zeroBN)) { - inaccessible = trade?.minInitialMargin ?? zeroBN; + // If the user has a position open, we'll enforce a min initial margin requirement. + if (inaccessible.gt(0)) { + if (inaccessible.lt(minInitialMargin)) { + inaccessible = minInitialMargin; + } } - } - // check if available margin will be less than 0 - return trade?.margin?.sub(inaccessible).gt(0) ? trade?.margin?.sub(inaccessible).abs() : zeroBN; - }; + // check if available margin will be less than 0 + return trade?.margin?.sub(inaccessible).gt(0) + ? trade?.margin?.sub(inaccessible).abs() + : zeroBN; + }, + [minInitialMargin] + ); const previewAvailableMargin = React.useMemo(() => { const potentialAvailableMargin = getPotentialAvailableMargin( - potentialTrade.data, + potentialTrade, marketInfo?.maxLeverage ); return isNextPriceOrder ? potentialAvailableMargin?.sub(totalDeposit) ?? zeroBN : potentialAvailableMargin; - }, [potentialTrade.data, marketInfo?.maxLeverage, isNextPriceOrder, totalDeposit]); + }, [ + potentialTrade, + marketInfo?.maxLeverage, + isNextPriceOrder, + totalDeposit, + getPotentialAvailableMargin, + ]); const previewTradeData = React.useMemo(() => { - const size = wei(nativeSize || zeroBN); + const size = nativeSizeDelta.abs(); - const potentialMarginUsage = potentialTrade.data?.margin.gt(0) - ? potentialTrade.data?.margin - ?.sub(previewAvailableMargin) - ?.div(potentialTrade.data?.margin) - ?.abs() ?? zeroBN + const potentialMarginUsage = potentialTrade?.margin.gt(0) + ? potentialTrade!.margin.sub(previewAvailableMargin).div(potentialTrade!.margin).abs() ?? + zeroBN : zeroBN; const potentialBuyingPower = @@ -103,12 +112,12 @@ const MarketInfoBox: React.FC = () => { return { showPreview: size && !size.eq(0), - totalMargin: potentialTrade.data?.margin || zeroBN, + totalMargin: potentialTrade?.margin || zeroBN, availableMargin: previewAvailableMargin.gt(0) ? previewAvailableMargin : zeroBN, buyingPower: potentialBuyingPower.gt(0) ? potentialBuyingPower : zeroBN, marginUsage: potentialMarginUsage.gt(1) ? wei(1) : potentialMarginUsage, }; - }, [nativeSize, potentialTrade.data?.margin, previewAvailableMargin, maxLeverage]); + }, [nativeSizeDelta, potentialTrade, previewAvailableMargin, maxLeverage]); return ( { currencyKey: undefined, })}`, valueNode: ( - + {formatDollars(previewTradeData?.availableMargin)} ), @@ -131,9 +138,7 @@ const MarketInfoBox: React.FC = () => { currencyKey: undefined, })}`, valueNode: previewTradeData?.buyingPower && ( - + {formatDollars(previewTradeData?.buyingPower)} ), @@ -141,9 +146,7 @@ const MarketInfoBox: React.FC = () => { 'Margin Usage': { value: `${formatPercent(marginUsage)}`, valueNode: ( - + {formatPercent(previewTradeData?.marginUsage)} ), diff --git a/sections/futures/MobileTrade/OverviewTabs/AccountTab.tsx b/sections/futures/MobileTrade/OverviewTabs/AccountTab.tsx index 9faec7eddd..5ccf78e6c9 100644 --- a/sections/futures/MobileTrade/OverviewTabs/AccountTab.tsx +++ b/sections/futures/MobileTrade/OverviewTabs/AccountTab.tsx @@ -1,15 +1,15 @@ import React from 'react'; -import { useRecoilValue } from 'recoil'; import MarketInfoBox from 'sections/futures/MarketInfoBox'; import MarketActions from 'sections/futures/Trade/MarketActions'; import MarginInfoBox from 'sections/futures/TradeCrossMargin/CrossMarginInfoBox'; -import { futuresAccountTypeState } from 'store/futures'; +import { selectFuturesType } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { Pane, SectionHeader, SectionTitle } from '../common'; const AccountTab: React.FC = () => { - const accountType = useRecoilValue(futuresAccountTypeState); + const accountType = useAppSelector(selectFuturesType); return ( diff --git a/sections/futures/MobileTrade/PositionDetails.tsx b/sections/futures/MobileTrade/PositionDetails.tsx index e368d2b21a..b72f52f6fc 100644 --- a/sections/futures/MobileTrade/PositionDetails.tsx +++ b/sections/futures/MobileTrade/PositionDetails.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; -import { positionState } from 'store/futures'; +import { selectPosition } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import PositionCard from '../PositionCard'; import { SectionHeader, SectionSeparator, SectionTitle } from './common'; const PositionDetails = () => { - const position = useRecoilValue(positionState); + const position = useAppSelector(selectPosition); return position?.position ? ( diff --git a/sections/futures/MobileTrade/UserTabs/UserTabs.tsx b/sections/futures/MobileTrade/UserTabs/UserTabs.tsx index a7e7760f79..c0f13e61a0 100644 --- a/sections/futures/MobileTrade/UserTabs/UserTabs.tsx +++ b/sections/futures/MobileTrade/UserTabs/UserTabs.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import TabButton from 'components/Button/TabButton'; import { FuturesAccountType } from 'queries/futures/subgraph'; import TradeIsolatedMargin from 'sections/futures/Trade/TradeIsolatedMargin'; import TradeCrossMargin from 'sections/futures/TradeCrossMargin'; -import { futuresAccountTypeState } from 'store/futures'; +import { selectFuturesType } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import OrdersTab from './OrdersTab'; import TradesTab from './TradesTab'; @@ -38,7 +38,7 @@ const getTabs = (accountType: FuturesAccountType) => [ const UserTabs: React.FC = () => { const [activeTab, setActiveTab] = React.useState(0); - const accountType = useRecoilValue(futuresAccountTypeState); + const accountType = useAppSelector(selectFuturesType); const tabs = getTabs(accountType); diff --git a/sections/futures/MobileTrade/drawers/OrderDrawer.tsx b/sections/futures/MobileTrade/drawers/OrderDrawer.tsx index c0aa293275..2c40fd639e 100644 --- a/sections/futures/MobileTrade/drawers/OrderDrawer.tsx +++ b/sections/futures/MobileTrade/drawers/OrderDrawer.tsx @@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next'; import styled, { css } from 'styled-components'; import Button from 'components/Button'; -import { FuturesOrder, PositionSide } from 'queries/futures/types'; +import { PositionSide } from 'queries/futures/types'; +import { FuturesOrder } from 'sdk/types/futures'; import { getDisplayAsset } from 'utils/futures'; import BaseDrawer from './BaseDrawer'; diff --git a/sections/futures/MobileTrade/drawers/TradeConfirmationDrawer.tsx b/sections/futures/MobileTrade/drawers/TradeConfirmationDrawer.tsx deleted file mode 100644 index f9f2ccd67e..0000000000 --- a/sections/futures/MobileTrade/drawers/TradeConfirmationDrawer.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React, { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; -import styled from 'styled-components'; - -import Button from 'components/Button'; -import { CurrencyKey } from 'constants/currency'; -import Connector from 'containers/Connector'; -import { useFuturesContext } from 'contexts/FuturesContext'; -import useEstimateGasCost from 'hooks/useEstimateGasCost'; -import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; -import { PositionSide } from 'sections/futures/types'; -import { selectMarketAsset } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { potentialTradeDetailsState } from 'store/futures'; -import { zeroBN, formatDollars, formatCurrency, formatNumber } from 'utils/formatters/number'; - -import BaseDrawer from './BaseDrawer'; - -type TradeConfirmationDrawerProps = { - open: boolean; - closeDrawer(): void; -}; - -const TradeConfirmationDrawer: React.FC = ({ open, closeDrawer }) => { - const { t } = useTranslation(); - const { synthsMap } = Connector.useContainer(); - const marketAsset = useAppSelector(selectMarketAsset); - const { selectedPriceCurrency } = useSelectedPriceCurrency(); - const { data: potentialTradeDetails } = useRecoilValue(potentialTradeDetailsState); - const { estimateSnxTxGasCost } = useEstimateGasCost(); - - const { orderTxn } = useFuturesContext(); - - const transactionFee = estimateSnxTxGasCost(orderTxn); - - const positionDetails = useMemo(() => { - return potentialTradeDetails - ? { - ...potentialTradeDetails, - size: potentialTradeDetails.size.abs(), - side: potentialTradeDetails.size.gte(zeroBN) ? PositionSide.LONG : PositionSide.SHORT, - leverage: potentialTradeDetails.margin.eq(zeroBN) - ? zeroBN - : potentialTradeDetails.size - .mul(potentialTradeDetails.price) - .div(potentialTradeDetails.margin) - .abs(), - } - : null; - }, [potentialTradeDetails]); - - const dataRows = useMemo( - () => [ - { label: 'side', value: (positionDetails?.side ?? PositionSide.LONG).toUpperCase() }, - { - label: 'size', - value: formatCurrency(marketAsset || '', positionDetails?.size ?? zeroBN, { - sign: marketAsset ? synthsMap[marketAsset]?.sign : '', - }), - }, - { label: 'leverage', value: `${formatNumber(positionDetails?.leverage ?? zeroBN)}x` }, - { - label: 'current price', - value: formatDollars(positionDetails?.price ?? zeroBN), - }, - { - label: 'liquidation price', - value: formatDollars(positionDetails?.liqPrice ?? zeroBN), - }, - { - label: 'margin', - value: formatDollars(positionDetails?.margin ?? zeroBN), - }, - { - label: 'protocol fee', - value: formatDollars(positionDetails?.fee ?? zeroBN), - }, - { - label: 'network gas fee', - value: formatCurrency(selectedPriceCurrency.name as CurrencyKey, transactionFee ?? zeroBN, { - sign: '$', - minDecimals: 2, - }), - }, - ], - [positionDetails, marketAsset, synthsMap, transactionFee, selectedPriceCurrency] - ); - - return ( - { - orderTxn.mutate(); - closeDrawer(); - }} - disabled={!positionDetails} - > - {t('futures.market.trade.confirmation.modal.confirm-order')} - - } - /> - ); -}; - -const ConfirmTradeButton = styled(Button)` - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - height: 45px; - width: 100%; -`; - -export default TradeConfirmationDrawer; diff --git a/sections/futures/OrderPriceInput/OrderPriceInput.tsx b/sections/futures/OrderPriceInput/OrderPriceInput.tsx index 8c7ee751e6..19fbb93a50 100644 --- a/sections/futures/OrderPriceInput/OrderPriceInput.tsx +++ b/sections/futures/OrderPriceInput/OrderPriceInput.tsx @@ -2,7 +2,7 @@ import { wei } from '@synthetixio/wei'; import { debounce } from 'lodash'; import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilState } from 'recoil'; import styled from 'styled-components'; import CustomInput from 'components/Input/CustomInput'; @@ -10,9 +10,13 @@ import InputTitle from 'components/Input/InputTitle'; import SegmentedControl from 'components/SegmentedControl'; import StyledTooltip from 'components/Tooltip/StyledTooltip'; import { FuturesOrderType } from 'queries/futures/types'; -import { selectMarketAssetRate, selectMarketInfo } from 'state/futures/selectors'; +import { + selectMarketAssetRate, + selectMarketInfo, + selectLeverageSide, +} from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { leverageSideState, orderFeeCapState } from 'store/futures'; +import { orderFeeCapState } from 'store/futures'; import { weiToString, zeroBN } from 'utils/formatters/number'; import { orderPriceInvalidLabel } from 'utils/futures'; @@ -33,7 +37,7 @@ export default function OrderPriceInput({ }: Props) { const { t } = useTranslation(); const marketAssetRate = useAppSelector(selectMarketAssetRate); - const leverageSide = useRecoilValue(leverageSideState); + const leverageSide = useAppSelector(selectLeverageSide); const [selectedFeeCap, setSelectedFeeCap] = useRecoilState(orderFeeCapState); const marketInfo = useAppSelector(selectMarketInfo); diff --git a/sections/futures/OrderSizing/OrderSizeSlider.tsx b/sections/futures/OrderSizing/OrderSizeSlider.tsx index 0f59d7716a..859ba55845 100644 --- a/sections/futures/OrderSizing/OrderSizeSlider.tsx +++ b/sections/futures/OrderSizing/OrderSizeSlider.tsx @@ -1,38 +1,36 @@ import { wei } from '@synthetixio/wei'; import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import ErrorView from 'components/Error'; import StyledSlider from 'components/Slider/StyledSlider'; import { useFuturesContext } from 'contexts/FuturesContext'; -import { selectMaxLeverage } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { editCrossMarginSize } from 'state/futures/actions'; import { - aboveMaxLeverageState, - crossMarginAccountOverviewState, - futuresTradeInputsState, - leverageSideState, - positionState, -} from 'store/futures'; + selectAboveMaxLeverage, + selectCrossMarginBalanceInfo, + selectLeverageSide, + selectMaxLeverage, + selectPosition, + selectTradeSizeInputs, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { FlexDivRow } from 'styles/common'; export default function OrderSizeSlider() { const { t } = useTranslation(); - const { onTradeAmountChange, maxUsdInputAmount, tradePrice } = useFuturesContext(); - - const { freeMargin: freeCrossMargin } = useRecoilValue(crossMarginAccountOverviewState); - const { susdSize } = useRecoilValue(futuresTradeInputsState); - const aboveMaxLeverage = useRecoilValue(aboveMaxLeverageState); - - const position = useRecoilValue(positionState); - const leverageSide = useRecoilValue(leverageSideState); + const { maxUsdInputAmount } = useFuturesContext(); + const dispatch = useAppDispatch(); + const { freeMargin: freeCrossMargin } = useAppSelector(selectCrossMarginBalanceInfo); + const { susdSizeString } = useAppSelector(selectTradeSizeInputs); + const aboveMaxLeverage = useAppSelector(selectAboveMaxLeverage); + const maxLeverage = useAppSelector(selectMaxLeverage); + const leverageSide = useAppSelector(selectLeverageSide); + const position = useAppSelector(selectPosition); const [percent, setPercent] = useState(0); - const [usdValue, setUsdValue] = useState(susdSize); - - const maxLeverage = useAppSelector(selectMaxLeverage); + const [usdValue, setUsdValue] = useState(susdSizeString); // eslint-disable-next-line const onChangeMarginPercent = useCallback( @@ -42,23 +40,25 @@ export default function OrderSizeSlider() { const usdAmount = maxUsdInputAmount.mul(fraction).toString(); const usdValue = Number(usdAmount).toFixed(0); setUsdValue(usdValue); - onTradeAmountChange(usdValue, tradePrice, 'usd', { simulateChange: !commit }); + if (commit) { + dispatch(editCrossMarginSize(usdValue, 'usd')); + } }, - [onTradeAmountChange, maxUsdInputAmount, tradePrice] + [maxUsdInputAmount, dispatch] ); useEffect(() => { - if (susdSize !== usdValue) { - if (!susdSize || maxUsdInputAmount.eq(0)) { + if (susdSizeString !== usdValue) { + if (!susdSizeString || maxUsdInputAmount.eq(0)) { setPercent(0); return; } - const percent = wei(susdSize).div(maxUsdInputAmount).mul(100).toNumber(); + const percent = wei(susdSizeString).div(maxUsdInputAmount).mul(100).toNumber(); setPercent(Number(percent.toFixed(2))); } // eslint-disable-next-line - }, [susdSize]); + }, [susdSizeString]); if (aboveMaxLeverage && position?.position?.side === leverageSide) { return ( diff --git a/sections/futures/OrderSizing/OrderSizing.tsx b/sections/futures/OrderSizing/OrderSizing.tsx index dc124a6b58..5bc1d4f33c 100644 --- a/sections/futures/OrderSizing/OrderSizing.tsx +++ b/sections/futures/OrderSizing/OrderSizing.tsx @@ -1,25 +1,24 @@ import { wei } from '@synthetixio/wei'; -import { debounce } from 'lodash'; -import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; -import { useRecoilValue } from 'recoil'; +import React, { ChangeEvent, useMemo, useState } from 'react'; import styled from 'styled-components'; import SwitchAssetArrows from 'assets/svg/futures/switch-arrows.svg'; import CustomInput from 'components/Input/CustomInput'; import InputTitle from 'components/Input/InputTitle'; import { useFuturesContext } from 'contexts/FuturesContext'; -import { selectMarketKey, selectMarketAssetRate } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { editTradeSizeInput } from 'state/futures/actions'; import { - futuresAccountTypeState, - simulatedTradeState, - positionState, - futuresTradeInputsState, - orderTypeState, - futuresOrderPriceState, - crossMarginAccountOverviewState, - leverageSideState, -} from 'store/futures'; + selectMarketKey, + selectMarketAssetRate, + selectCrossMarginBalanceInfo, + selectPosition, + selectTradeSizeInputs, + selectCrossMarginOrderPrice, + selectOrderType, + selectLeverageSide, + selectFuturesType, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { FlexDivRow } from 'styles/common'; import { floorNumber, isZero, zeroBN } from 'utils/formatters/number'; import { getDisplayAsset } from 'utils/futures'; @@ -32,23 +31,21 @@ type OrderSizingProps = { }; const OrderSizing: React.FC = ({ disabled, isMobile }) => { - const { onTradeAmountChange, maxUsdInputAmount } = useFuturesContext(); + const { maxUsdInputAmount } = useFuturesContext(); + const dispatch = useAppDispatch(); - const { nativeSize, susdSize } = useRecoilValue(futuresTradeInputsState); - const simulatedTrade = useRecoilValue(simulatedTradeState); + const { susdSizeString, nativeSizeString } = useAppSelector(selectTradeSizeInputs); - const { freeMargin: freeCrossMargin } = useRecoilValue(crossMarginAccountOverviewState); - const position = useRecoilValue(positionState); - const selectedAccountType = useRecoilValue(futuresAccountTypeState); - const orderType = useRecoilValue(orderTypeState); + const { freeMargin: freeCrossMargin } = useAppSelector(selectCrossMarginBalanceInfo); + const position = useAppSelector(selectPosition); + const selectedAccountType = useAppSelector(selectFuturesType); + const orderType = useAppSelector(selectOrderType); const marketAssetRate = useAppSelector(selectMarketAssetRate); - const orderPrice = useRecoilValue(futuresOrderPriceState); - const selectedLeverageSide = useRecoilValue(leverageSideState); + const orderPrice = useAppSelector(selectCrossMarginOrderPrice); + const selectedLeverageSide = useAppSelector(selectLeverageSide); const marketKey = useAppSelector(selectMarketKey); - const [usdValue, setUsdValue] = useState(susdSize); - const [assetValue, setAssetValue] = useState(nativeSize); const [assetInputType, setAssetInputType] = useState<'usd' | 'native'>('usd'); const tradePrice = useMemo(() => (orderPrice ? wei(orderPrice) : marketAssetRate), [ @@ -60,59 +57,24 @@ const OrderSizing: React.FC = ({ disabled, isMobile }) => { [tradePrice, maxUsdInputAmount] ); - useEffect( - () => { - if (simulatedTrade && simulatedTrade.susdSize !== susdSize) { - setUsdValue(simulatedTrade.susdSize); - } else if (susdSize !== usdValue) { - setUsdValue(susdSize); - } - - if (simulatedTrade && simulatedTrade.nativeSize !== nativeSize) { - setAssetValue(simulatedTrade.nativeSize); - } else if (assetValue !== nativeSize) { - setAssetValue(nativeSize); - } - }, - // Don't want to react to internal value changes - // eslint-disable-next-line - [ - susdSize, - nativeSize, - simulatedTrade?.susdSize, - simulatedTrade?.nativeSize, - setUsdValue, - setAssetValue, - ] - ); + const onSizeChange = (value: string, assetType: 'native' | 'usd') => { + dispatch(editTradeSizeInput(value, assetType)); + }; const handleSetMax = () => { if (assetInputType === 'usd') { - onTradeAmountChange(String(floorNumber(maxUsdInputAmount)), tradePrice, 'usd'); + onSizeChange(String(floorNumber(maxUsdInputAmount)), 'usd'); } else { - onTradeAmountChange(String(floorNumber(maxNativeValue)), tradePrice, 'native'); + onSizeChange(String(floorNumber(maxNativeValue)), 'native'); } }; const handleSetPositionSize = () => { - onTradeAmountChange(position?.position?.size.toString() ?? '0', tradePrice, 'native'); + onSizeChange(position?.position?.size.toString() ?? '0', 'native'); }; - // eslint-disable-next-line - const debounceOnChangeValue = useCallback( - debounce((value, assetType) => { - onTradeAmountChange(value, tradePrice, assetType); - }, 500), - [debounce, onTradeAmountChange] - ); - - useEffect(() => { - return () => debounceOnChangeValue?.cancel(); - }, [debounceOnChangeValue]); - const onChangeValue = (_: ChangeEvent, v: string) => { - assetInputType === 'usd' ? setUsdValue(v) : setAssetValue(v); - debounceOnChangeValue(v, assetInputType); + dispatch(editTradeSizeInput(v, assetInputType)); }; const isDisabled = useMemo(() => { @@ -129,8 +91,12 @@ const OrderSizing: React.FC = ({ disabled, isMobile }) => { position?.position.side !== selectedLeverageSide; const invalid = - (assetInputType === 'usd' && usdValue !== '' && maxUsdInputAmount.lte(usdValue || 0)) || - (assetInputType === 'native' && assetValue !== '' && maxNativeValue.lte(assetValue || 0)); + (assetInputType === 'usd' && + susdSizeString !== '' && + maxUsdInputAmount.lte(susdSizeString || 0)) || + (assetInputType === 'native' && + nativeSizeString !== '' && + maxNativeValue.lte(nativeSizeString || 0)); return ( <> @@ -159,7 +125,7 @@ const OrderSizing: React.FC = ({ disabled, isMobile }) => { {} } - value={assetInputType === 'usd' ? usdValue : assetValue} + value={assetInputType === 'usd' ? susdSizeString : nativeSizeString} placeholder="0.00" onChange={onChangeValue} /> diff --git a/sections/futures/PositionCard/ClosePositionModal.tsx b/sections/futures/PositionCard/ClosePositionModal.tsx index 4e39ba36e9..1987934d5a 100644 --- a/sections/futures/PositionCard/ClosePositionModal.tsx +++ b/sections/futures/PositionCard/ClosePositionModal.tsx @@ -1,4 +1,3 @@ -import { CurrencyKey } from '@synthetixio/contracts-interface'; import Wei, { wei } from '@synthetixio/wei'; import { useMemo, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -7,11 +6,11 @@ import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; import Button from 'components/Button'; import Error from 'components/Error'; +import { ButtonLoader } from 'components/Loader/Loader'; import Connector from 'containers/Connector'; -import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; -import { FuturesFilledPosition } from 'queries/futures/types'; import { getFuturesMarketContract } from 'queries/futures/utils'; -import { selectMarketAsset } from 'state/futures/selectors'; +import { FuturesFilledPosition } from 'sdk/types/futures'; +import { selectIsClosingPosition, selectMarketAsset } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; import { FlexDivCentered, FlexDivCol } from 'styles/common'; import { formatCurrency, formatDollars, formatNumber, zeroBN } from 'utils/formatters/number'; @@ -19,7 +18,6 @@ import { formatCurrency, formatDollars, formatNumber, zeroBN } from 'utils/forma import { PositionSide } from '../types'; type ClosePositionModalProps = { - gasFee: Wei | null; positionDetails: FuturesFilledPosition | null | undefined; disabled?: boolean; errorMessage?: string | null | undefined; @@ -28,7 +26,6 @@ type ClosePositionModalProps = { }; function ClosePositionModal({ - gasFee, positionDetails, disabled, errorMessage, @@ -37,9 +34,9 @@ function ClosePositionModal({ }: ClosePositionModalProps) { const { t } = useTranslation(); const { defaultSynthetixjs: synthetixjs, synthsMap } = Connector.useContainer(); - const { selectedPriceCurrency } = useSelectedPriceCurrency(); const marketAsset = useAppSelector(selectMarketAsset); + const isClosing = useAppSelector(selectIsClosingPosition); const [orderFee, setOrderFee] = useState(wei(0)); const [error, setError] = useState(null); @@ -94,14 +91,8 @@ function ClosePositionModal({ label: t('futures.market.user.position.modal.fee'), value: formatDollars(orderFee), }, - { - label: t('futures.market.user.position.modal.gas-fee'), - value: formatCurrency(selectedPriceCurrency.name as CurrencyKey, gasFee ?? zeroBN, { - sign: '$', - }), - }, ]; - }, [positionDetails, marketAsset, t, orderFee, gasFee, selectedPriceCurrency, synthsMap]); + }, [positionDetails, marketAsset, t, orderFee, synthsMap]); return ( - {t('futures.market.user.position.modal.title')} + {isClosing ? : t('futures.market.user.position.modal.title')} {errorMessage && } @@ -166,7 +157,6 @@ const ValueColumn = styled(FlexDivCol)` const StyledButton = styled(Button)` margin-top: 24px; - margin-bottom: 16px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; diff --git a/sections/futures/PositionCard/ClosePositionModalCrossMargin.tsx b/sections/futures/PositionCard/ClosePositionModalCrossMargin.tsx index 654769f980..9cc9a43cf1 100644 --- a/sections/futures/PositionCard/ClosePositionModalCrossMargin.tsx +++ b/sections/futures/PositionCard/ClosePositionModalCrossMargin.tsx @@ -2,7 +2,6 @@ import Wei, { wei } from '@synthetixio/wei'; import { formatBytes32String } from 'ethers/lib/utils'; import { useMemo, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import { DEFAULT_CROSSMARGIN_GAS_BUFFER_PCT } from 'constants/defaults'; import { useFuturesContext } from 'contexts/FuturesContext'; @@ -10,9 +9,9 @@ import { useRefetchContext } from 'contexts/RefetchContext'; import { monitorTransaction } from 'contexts/RelayerContext'; import useCrossMarginAccountContracts from 'hooks/useCrossMarginContracts'; import useEstimateGasCost from 'hooks/useEstimateGasCost'; -import { selectMarketAsset, selectMarketKey } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { positionState } from 'store/futures'; +import { fetchCrossMarginBalanceInfo } from 'state/futures/actions'; +import { selectMarketAsset, selectMarketKey, selectPosition } from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { isUserDeniedError } from 'utils/formatters/error'; import { zeroBN } from 'utils/formatters/number'; import logError from 'utils/logError'; @@ -26,19 +25,19 @@ type Props = { export default function ClosePositionModalCrossMargin({ onDismiss }: Props) { const { t } = useTranslation(); - const { handleRefetch, refetchUntilUpdate } = useRefetchContext(); + const { handleRefetch } = useRefetchContext(); const { crossMarginAccountContract } = useCrossMarginAccountContracts(); const { resetTradeState } = useFuturesContext(); const { estimateEthersContractTxCost } = useEstimateGasCost(); + const dispatch = useAppDispatch(); - const [crossMarginGasFee, setCrossMarginGasFee] = useState(null); const [crossMarginGasLimit, setCrossMarginGasLimit] = useState(null); const [error, setError] = useState(null); const marketAsset = useAppSelector(selectMarketAsset); const marketKey = useAppSelector(selectMarketKey); - const position = useRecoilValue(positionState); + const position = useAppSelector(selectPosition); const positionDetails = position?.position; const positionSize = useMemo(() => positionDetails?.size ?? zeroBN, [positionDetails?.size]); @@ -73,13 +72,12 @@ export default function ClosePositionModalCrossMargin({ onDismiss }: Props) { useEffect(() => { if (!crossMarginAccountContract) return; const estimateGas = async () => { - const { gasPrice, gasLimit } = await estimateEthersContractTxCost( + const { gasLimit } = await estimateEthersContractTxCost( crossMarginAccountContract, 'distributeMargin', [crossMarginCloseParams], DEFAULT_CROSSMARGIN_GAS_BUFFER_PCT ); - setCrossMarginGasFee(gasPrice); setCrossMarginGasLimit(gasLimit); }; estimateGas(); @@ -93,7 +91,7 @@ export default function ClosePositionModalCrossMargin({ onDismiss }: Props) { onDismiss(); resetTradeState(); handleRefetch('close-position'); - refetchUntilUpdate('account-margin-change'); + dispatch(fetchCrossMarginBalanceInfo()); }, }); } @@ -118,7 +116,6 @@ export default function ClosePositionModalCrossMargin({ onDismiss }: Props) { return ( { - if (closeTxn?.hash) { - monitorTx(closeTxn.hash); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [closeTxn?.hash]); - - const monitorTx = (txHash: string) => { - if (txHash) { - monitorTransaction({ - txHash: txHash, - onTxConfirmed: () => { - onDismiss(); - resetTradeState(); - handleRefetch('close-position'); - refetchUntilUpdate('account-margin-change'); - }, - }); - } - }; return ( closeTxn?.mutate()} + onClosePosition={() => dispatch(closeIsolatedMarginPosition())} /> ); } diff --git a/sections/futures/PositionCard/PositionCard.tsx b/sections/futures/PositionCard/PositionCard.tsx index de8c32de8a..c443c74895 100644 --- a/sections/futures/PositionCard/PositionCard.tsx +++ b/sections/futures/PositionCard/PositionCard.tsx @@ -14,13 +14,15 @@ import useAverageEntryPrice from 'hooks/useAverageEntryPrice'; import useFuturesMarketClosed from 'hooks/useFuturesMarketClosed'; import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; import { PositionSide } from 'queries/futures/types'; -import { selectMarketAsset, selectMarketKey, selectPosition } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; import { - futuresAccountTypeState, - positionHistoryState, - potentialTradeDetailsState, -} from 'store/futures'; + selectMarketAsset, + selectMarketKey, + selectPosition, + selectTradePreview, + selectFuturesType, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; +import { positionHistoryState } from 'store/futures'; import { FlexDivCentered, FlexDivCol, PillButtonDiv } from 'styles/common'; import media from 'styles/media'; import { isFiatCurrency } from 'utils/currencies'; @@ -28,7 +30,7 @@ import { formatDollars, formatPercent, zeroBN } from 'utils/formatters/number'; import { formatNumber } from 'utils/formatters/number'; import { getMarketName, getSynthDescription, isDecimalFour, MarketKeyByAsset } from 'utils/futures'; -import EditLeverageModal from '../TradeCrossMargin/EditLeverageModal'; +import EditLeverageModal from '../TradeCrossMargin/EditCrossMarginLeverageModal'; type PositionCardProps = { dashboard?: boolean; @@ -65,22 +67,22 @@ type PositionPreviewData = { const PositionCard: React.FC = () => { const { t } = useTranslation(); - const futuresAccountType = useRecoilValue(futuresAccountTypeState); + const { synthsMap } = Connector.useContainer(); + const { marketAssetRate } = useFuturesContext(); + const { selectedPriceCurrency } = useSelectedPriceCurrency(); + const futuresAccountType = useAppSelector(selectFuturesType); const position = useAppSelector(selectPosition); const marketAsset = useAppSelector(selectMarketAsset); const marketKey = useAppSelector(selectMarketKey); - - const positionDetails = position?.position ?? null; + const positionHistory = useRecoilValue(positionHistoryState); + const previewTradeData = useAppSelector(selectTradePreview); const { isFuturesMarketClosed } = useFuturesMarketClosed(marketKey); - const positionHistory = useRecoilValue(positionHistoryState); + const positionDetails = position?.position ?? null; const [showEditLeverage, setShowEditLeverage] = useState(false); - const { synthsMap } = Connector.useContainer(); - - const { selectedPriceCurrency } = useSelectedPriceCurrency(); const minDecimals = isFiatCurrency(selectedPriceCurrency.name) && isDecimalFour(marketKey) ? DEFAULT_CRYPTO_DECIMALS @@ -92,10 +94,6 @@ const PositionCard: React.FC = () => { ); }, [positionHistory, marketAsset, futuresAccountType]); - const { marketAssetRate } = useFuturesContext(); - - const { data: previewTradeData } = useRecoilValue(potentialTradeDetailsState); - const modifiedAverage = useAverageEntryPrice(thisPositionHistory); const previewData: PositionPreviewData = React.useMemo(() => { diff --git a/sections/futures/PositionChart/PositionChart.tsx b/sections/futures/PositionChart/PositionChart.tsx index 87f1c95acb..94398c3f5d 100644 --- a/sections/futures/PositionChart/PositionChart.tsx +++ b/sections/futures/PositionChart/PositionChart.tsx @@ -4,27 +4,26 @@ import styled from 'styled-components'; import TVChart from 'components/TVChart'; import useAverageEntryPrice from 'hooks/useAverageEntryPrice'; -import { selectMarketAsset } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; import { - positionState, - potentialTradeDetailsState, - positionHistoryState, - futuresAccountTypeState, - openOrdersState, -} from 'store/futures'; + selectFuturesType, + selectMarketAsset, + selectOpenOrders, + selectPosition, + selectTradePreview, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; +import { positionHistoryState } from 'store/futures'; export default function PositionChart() { - const [isChartReady, setIsChartReady] = useState(false); const marketAsset = useAppSelector(selectMarketAsset); - - const position = useRecoilValue(positionState); + const position = useAppSelector(selectPosition); const positionHistory = useRecoilValue(positionHistoryState); - const futuresAccountType = useRecoilValue(futuresAccountTypeState); - const openOrders = useRecoilValue(openOrdersState); - const { data: previewTrade } = useRecoilValue(potentialTradeDetailsState); + const futuresAccountType = useAppSelector(selectFuturesType); + const openOrders = useAppSelector(selectOpenOrders); + const previewTrade = useAppSelector(selectTradePreview); const [showOrderLines, setShowOrderLines] = useState(true); + const [isChartReady, setIsChartReady] = useState(false); const subgraphPosition = useMemo(() => { return positionHistory[futuresAccountType].find((p) => p.isOpen && p.asset === marketAsset); diff --git a/sections/futures/ShareModal/AmountContainer.tsx b/sections/futures/ShareModal/AmountContainer.tsx index 51a2a28d4f..c75a0d531b 100644 --- a/sections/futures/ShareModal/AmountContainer.tsx +++ b/sections/futures/ShareModal/AmountContainer.tsx @@ -2,7 +2,7 @@ import { FC } from 'react'; import styled from 'styled-components'; import CurrencyIcon from 'components/Currency/CurrencyIcon'; -import { FuturesPosition } from 'queries/futures/types'; +import { FuturesPosition } from 'sdk/types/futures'; import { selectMarketAsset } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; import { formatNumber, zeroBN } from 'utils/formatters/number'; diff --git a/sections/futures/ShareModal/PositionMetadata.tsx b/sections/futures/ShareModal/PositionMetadata.tsx index 958bc394ec..65ff868285 100644 --- a/sections/futures/ShareModal/PositionMetadata.tsx +++ b/sections/futures/ShareModal/PositionMetadata.tsx @@ -5,7 +5,9 @@ import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import { useFuturesContext } from 'contexts/FuturesContext'; -import { futuresAccountTypeState, positionHistoryState } from 'store/futures'; +import { selectFuturesType } from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; +import { positionHistoryState } from 'store/futures'; import getLocale from 'utils/formatters/getLocale'; type PositionMetadataProps = { @@ -72,7 +74,7 @@ const PositionMetadata: FC = ({ marketAsset }) => { const { marketAssetRate } = useFuturesContext(); const futuresPositionHistory = useRecoilValue(positionHistoryState); - const futuresAccountType = useRecoilValue(futuresAccountTypeState); + const futuresAccountType = useAppSelector(selectFuturesType); let avgEntryPrice = '', openAtDate = '', diff --git a/sections/futures/ShareModal/ShareModal.tsx b/sections/futures/ShareModal/ShareModal.tsx index 2b34baaf98..61c4b673b2 100644 --- a/sections/futures/ShareModal/ShareModal.tsx +++ b/sections/futures/ShareModal/ShareModal.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components'; import PNLGraphicPNG from 'assets/png/pnl-graphic.png'; import BaseModal from 'components/BaseModal'; -import { FuturesPosition } from 'queries/futures/types'; +import { FuturesPosition } from 'sdk/types/futures'; import { FuturesMarketAsset } from 'utils/futures'; import AmountContainer from './AmountContainer'; diff --git a/sections/futures/Trade/ManagePosition.tsx b/sections/futures/Trade/ManagePosition.tsx index fb0e8f6af1..0633edf582 100644 --- a/sections/futures/Trade/ManagePosition.tsx +++ b/sections/futures/Trade/ManagePosition.tsx @@ -1,6 +1,5 @@ import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue, useRecoilState } from 'recoil'; import styled from 'styled-components'; import Button from 'components/Button'; @@ -9,28 +8,31 @@ import Loader from 'components/Loader'; import { useFuturesContext } from 'contexts/FuturesContext'; import { previewErrorI18n } from 'queries/futures/constants'; import { PositionSide } from 'queries/futures/types'; -import { setLeverageSide as setReduxLeverageSide } from 'state/futures/reducer'; +import { setOpenModal } from 'state/app/reducer'; +import { selectOpenModal } from 'state/app/selectors'; +import { changeLeverageSide, editTradeSizeInput } from 'state/futures/actions'; import { selectMarketInfo, selectIsMarketCapReached, selectMarketAssetRate, selectPlaceOrderTranslationKey, + selectPosition, selectMaxLeverage, + selectFuturesTransaction, + selectTradePreviewError, + selectTradePreview, + selectTradePreviewStatus, + selectTradeSizeInputs, + selectIsolatedMarginLeverage, + selectCrossMarginOrderPrice, + selectOrderType, + selectIsAdvancedOrder, + selectFuturesType, + selectCrossMarginMarginDelta, + selectLeverageSide, } from 'state/futures/selectors'; import { useAppDispatch, useAppSelector } from 'state/hooks'; -import { - confirmationModalOpenState, - leverageSideState, - orderTypeState, - positionState, - potentialTradeDetailsState, - sizeDeltaState, - futuresTradeInputsState, - futuresAccountTypeState, - crossMarginMarginDeltaState, - futuresOrderPriceState, - isAdvancedOrderState, -} from 'store/futures'; +import { FetchStatus } from 'state/types'; import { isZero } from 'utils/formatters/number'; import { orderPriceInvalidLabel } from 'utils/futures'; @@ -40,60 +42,48 @@ import NextPriceConfirmationModal from './NextPriceConfirmationModal'; import TradeConfirmationModalCrossMargin from './TradeConfirmationModalCrossMargin'; import TradeConfirmationModalIsolatedMargin from './TradeConfirmationModalIsolatedMargin'; -type OrderTxnError = { - reason: string; -}; - const ManagePosition: React.FC = () => { const { t } = useTranslation(); - const { - error, - orderTxn, - onTradeAmountChange, - maxUsdInputAmount, - tradePrice, - } = useFuturesContext(); + const dispatch = useAppDispatch(); - const sizeDelta = useRecoilValue(sizeDeltaState); - const marginDelta = useRecoilValue(crossMarginMarginDeltaState); - const position = useRecoilValue(positionState); - const selectedAccountType = useRecoilValue(futuresAccountTypeState); - const { data: previewTrade, error: previewError, status } = useRecoilValue( - potentialTradeDetailsState - ); - const orderType = useRecoilValue(orderTypeState); - const [leverageSide, setLeverageSide] = useRecoilState(leverageSideState); - const { leverage } = useRecoilValue(futuresTradeInputsState); - const [isConfirmationModalOpen, setConfirmationModalOpen] = useRecoilState( - confirmationModalOpenState - ); + const { maxUsdInputAmount } = useFuturesContext(); + + const { susdSize } = useAppSelector(selectTradeSizeInputs); + const marginDelta = useAppSelector(selectCrossMarginMarginDelta); + const position = useAppSelector(selectPosition); + const maxLeverageValue = useAppSelector(selectMaxLeverage); + const selectedAccountType = useAppSelector(selectFuturesType); + const previewTrade = useAppSelector(selectTradePreview); + + const previewError = useAppSelector(selectTradePreviewError); + const leverage = useAppSelector(selectIsolatedMarginLeverage); + const orderType = useAppSelector(selectOrderType); + const leverageSide = useAppSelector(selectLeverageSide); + + const futuresTransaction = useAppSelector(selectFuturesTransaction); const isMarketCapReached = useAppSelector(selectIsMarketCapReached); const placeOrderTranslationKey = useAppSelector(selectPlaceOrderTranslationKey); - const dispatch = useAppDispatch(); - const orderPrice = useRecoilValue(futuresOrderPriceState); + const orderPrice = useAppSelector(selectCrossMarginOrderPrice); const marketAssetRate = useAppSelector(selectMarketAssetRate); - const tradeInputs = useRecoilValue(futuresTradeInputsState); - const isAdvancedOrder = useRecoilValue(isAdvancedOrderState); - + const isAdvancedOrder = useAppSelector(selectIsAdvancedOrder); + const openModal = useAppSelector(selectOpenModal); const marketInfo = useAppSelector(selectMarketInfo); - const maxLeverageValue = useAppSelector(selectMaxLeverage); + const previewStatus = useAppSelector(selectTradePreviewStatus); - const [isCancelModalOpen, setCancelModalOpen] = React.useState(false); + const isCancelModalOpen = openModal === 'futures_close_position_confirm'; + const isConfirmationModalOpen = openModal === 'futures_modify_position_confirm'; const positionDetails = position?.position; const orderError = useMemo(() => { if (previewError) return t(previewErrorI18n(previewError)); - const orderTxnError = orderTxn.error as OrderTxnError; - if (orderTxnError?.reason) return orderTxnError.reason; - if (error) return error; + if (futuresTransaction?.error) return futuresTransaction.error; if (previewTrade?.showStatus) return previewTrade?.statusMessage; return null; }, [ - orderTxn.error, - error, previewTrade?.showStatus, previewTrade?.statusMessage, + futuresTransaction?.error, previewError, t, ]); @@ -102,7 +92,7 @@ const ManagePosition: React.FC = () => { if (selectedAccountType === 'cross_margin') return true; const leverageNum = Number(leverage || 0); return leverageNum > 0 && leverageNum < maxLeverageValue.toNumber(); - }, [leverage, selectedAccountType, maxLeverageValue]); + }, [selectedAccountType, maxLeverageValue, leverage]); const placeOrderDisabledReason = useMemo(() => { const invalidReason = orderPriceInvalidLabel( @@ -112,27 +102,25 @@ const ManagePosition: React.FC = () => { orderType ); if (!leverageValid) return 'invalid_leverage'; - if (!!error) return error; if (marketInfo?.isSuspended) return 'market_suspended'; if (isMarketCapReached) return 'market_cap_reached'; if ((orderType === 'limit' || orderType === 'stop market') && !!invalidReason) return invalidReason; - if (tradeInputs.susdSizeDelta.abs().gt(maxUsdInputAmount)) return 'max_size_exceeded'; + if (susdSize.gt(maxUsdInputAmount)) return 'max_size_exceeded'; if (placeOrderTranslationKey === 'futures.market.trade.button.deposit-margin-minimum') return 'min_margin_required'; if (selectedAccountType === 'cross_margin') { - if ((isZero(marginDelta) && isZero(sizeDelta)) || status !== 'complete') + if ((isZero(marginDelta) && isZero(susdSize)) || previewStatus.status !== FetchStatus.Success) return 'awaiting_preview'; - if (orderType !== 'market' && isZero(orderPrice)) return 'price_required'; - } else if (isZero(sizeDelta)) { + if (orderType !== 'market' && isZero(orderPrice)) return 'pricerequired'; + } else if (isZero(susdSize)) { return 'size_required'; } return null; }, [ leverageValid, - error, - sizeDelta, + susdSize, marginDelta, orderType, orderPrice, @@ -140,11 +128,10 @@ const ManagePosition: React.FC = () => { marketAssetRate, marketInfo?.isSuspended, placeOrderTranslationKey, - tradeInputs.susdSizeDelta, maxUsdInputAmount, selectedAccountType, isMarketCapReached, - status, + previewStatus, ]); // TODO: Better user feedback for disabled reasons @@ -162,9 +149,13 @@ const ManagePosition: React.FC = () => { noOutline fullWidth disabled={!!placeOrderDisabledReason} - onClick={() => setConfirmationModalOpen(true)} + onClick={() => dispatch(setOpenModal('futures_modify_position_confirm'))} > - {status === 'fetching' ? : t(placeOrderTranslationKey)} + {previewStatus.status === FetchStatus.Loading ? ( + + ) : ( + t(placeOrderTranslationKey) + )} { position.position.side === PositionSide.LONG ? PositionSide.SHORT : PositionSide.LONG; - setLeverageSide(newLeverageSide); - dispatch(setReduxLeverageSide(newLeverageSide)); - onTradeAmountChange(newTradeSize.toString(), tradePrice, 'native'); - setConfirmationModalOpen(true); + dispatch(changeLeverageSide(newLeverageSide)); + dispatch(editTradeSizeInput(newTradeSize.toString(), 'native')); + dispatch(setOpenModal('futures_modify_position_confirm')); } else { - setCancelModalOpen(true); + dispatch(setOpenModal('futures_close_position_confirm')); } }} disabled={!positionDetails || marketInfo?.isSuspended || isAdvancedOrder} @@ -195,24 +185,24 @@ const ManagePosition: React.FC = () => { {orderError && ( - + )} {isCancelModalOpen && (selectedAccountType === 'cross_margin' ? ( - setCancelModalOpen(false)} /> + dispatch(setOpenModal(null))} /> ) : ( - setCancelModalOpen(false)} /> + dispatch(setOpenModal(null))} /> ))} {isConfirmationModalOpen && (selectedAccountType === 'cross_margin' ? ( + ) : orderType === 'next price' ? ( + ) : ( ))} - - {isConfirmationModalOpen && orderType === 'next price' && } ); }; diff --git a/sections/futures/Trade/MarketActions.tsx b/sections/futures/Trade/MarketActions.tsx index 83eff95d90..6ae6f3cddd 100644 --- a/sections/futures/Trade/MarketActions.tsx +++ b/sections/futures/Trade/MarketActions.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import Button from 'components/Button'; import Connector from 'containers/Connector'; import useIsL2 from 'hooks/useIsL2'; -import { selectMarketInfo } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { balancesState, positionState } from 'store/futures'; +import { setOpenModal } from 'state/app/reducer'; +import { selectOpenModal } from 'state/app/selectors'; +import { selectMarketInfo, selectPosition } from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { zeroBN } from 'utils/formatters/number'; import TransferIsolatedMarginModal from './TransferIsolatedMarginModal'; @@ -16,12 +16,12 @@ import TransferIsolatedMarginModal from './TransferIsolatedMarginModal'; const MarketActions: React.FC = () => { const { t } = useTranslation(); const { walletAddress } = Connector.useContainer(); - const { susdWalletBalance } = useRecoilValue(balancesState); - - const position = useRecoilValue(positionState); + const dispatch = useAppDispatch(); + const position = useAppSelector(selectPosition); const marketInfo = useAppSelector(selectMarketInfo); + const openModal = useAppSelector(selectOpenModal); + const isL2 = useIsL2(); - const [openModal, setOpenModal] = React.useState<'deposit' | 'withdraw' | null>(null); return ( <> @@ -29,7 +29,7 @@ const MarketActions: React.FC = () => { setOpenModal('deposit')} + onClick={() => dispatch(setOpenModal('futures_isolated_transfer'))} noOutline > {t('futures.market.trade.button.deposit')} @@ -42,25 +42,23 @@ const MarketActions: React.FC = () => { !isL2 || !walletAddress } - onClick={() => setOpenModal('withdraw')} + onClick={() => dispatch(setOpenModal('futures_isolated_transfer'))} noOutline > {t('futures.market.trade.button.withdraw')} - {openModal === 'deposit' && ( + {openModal === 'futures_isolated_transfer' && ( setOpenModal(null)} + onDismiss={() => dispatch(setOpenModal(null))} /> )} - {openModal === 'withdraw' && ( + {openModal === 'futures_isolated_transfer' && ( setOpenModal(null)} + onDismiss={() => dispatch(setOpenModal(null))} /> )} diff --git a/sections/futures/Trade/MarketsDropdown.tsx b/sections/futures/Trade/MarketsDropdown.tsx index 0da5fed5eb..cbdee6ebc9 100644 --- a/sections/futures/Trade/MarketsDropdown.tsx +++ b/sections/futures/Trade/MarketsDropdown.tsx @@ -17,10 +17,11 @@ import { selectMarketAsset, selectMarkets, selectMarketsQueryStatus, + selectFuturesType, } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; import { FetchStatus } from 'state/types'; -import { futuresAccountTypeState, pastRatesState } from 'store/futures'; +import { pastRatesState } from 'store/futures'; import { assetToSynth, iStandardSynth } from 'utils/currencies'; import { formatCurrency, formatPercent, zeroBN } from 'utils/formatters/number'; import { @@ -68,7 +69,7 @@ type MarketsDropdownProps = { const MarketsDropdown: React.FC = ({ mobile }) => { const pastRates = useRecoilValue(pastRatesState); - const accountType = useRecoilValue(futuresAccountTypeState); + const accountType = useAppSelector(selectFuturesType); const marketAsset = useAppSelector(selectMarketAsset); const futuresMarkets = useAppSelector(selectMarkets); const marketsQueryStatus = useAppSelector(selectMarketsQueryStatus); @@ -151,7 +152,7 @@ const MarketsDropdown: React.FC = ({ mobile }) => { getMinDecimals, ]); - const isFetching = !futuresMarkets.length && marketsQueryStatus === FetchStatus.Loading; + const isFetching = !futuresMarkets.length && marketsQueryStatus.status === FetchStatus.Loading; return ( diff --git a/sections/futures/Trade/NextPriceConfirmationModal.tsx b/sections/futures/Trade/NextPriceConfirmationModal.tsx index 4ec8df12be..4ea7e9819b 100644 --- a/sections/futures/Trade/NextPriceConfirmationModal.tsx +++ b/sections/futures/Trade/NextPriceConfirmationModal.tsx @@ -1,28 +1,30 @@ import useSynthetixQueries from '@synthetixio/queries'; import { wei } from '@synthetixio/wei'; -import { FC, useMemo } from 'react'; +import { FC, useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; import Button from 'components/Button'; +import { ButtonLoader } from 'components/Loader/Loader'; import { DesktopOnlyView, MobileOrTabletView } from 'components/Media'; import { NO_VALUE } from 'constants/placeholder'; import Connector from 'containers/Connector'; -import { useFuturesContext } from 'contexts/FuturesContext'; -import useEstimateGasCost from 'hooks/useEstimateGasCost'; import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; import GasPriceSelect from 'sections/shared/components/GasPriceSelect'; -import { selectMarketAsset, selectMarketInfo } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { setOpenModal } from 'state/app/reducer'; +import { modifyIsolatedPosition, modifyIsolatedPositionEstimateGas } from 'state/futures/actions'; import { - confirmationModalOpenState, - leverageSideState, - nextPriceDisclaimerState, - positionState, - futuresTradeInputsState, -} from 'store/futures'; + selectIsModifyingIsolatedPosition, + selectLeverageSide, + selectMarketAsset, + selectMarketInfo, + selectModifyIsolatedGasEstimate, + selectNextPriceDisclaimer, + selectPosition, + selectTradeSizeInputs, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { FlexDivCol, FlexDivCentered } from 'styles/common'; import { computeNPFee } from 'utils/costCalculations'; import { zeroBN, formatCurrency, formatDollars } from 'utils/formatters/number'; @@ -34,28 +36,35 @@ import { MobileConfirmTradeButton } from './TradeConfirmationModal'; const NextPriceConfirmationModal: FC = () => { const { t } = useTranslation(); const { synthsMap } = Connector.useContainer(); - const isDisclaimerDisplayed = useRecoilValue(nextPriceDisclaimerState); + const isDisclaimerDisplayed = useAppSelector(selectNextPriceDisclaimer); const { useEthGasPriceQuery } = useSynthetixQueries(); const { selectedPriceCurrency } = useSelectedPriceCurrency(); const ethGasPriceQuery = useEthGasPriceQuery(); - const { estimateSnxTxGasCost } = useEstimateGasCost(); + const dispatch = useAppDispatch(); - const { nativeSize } = useRecoilValue(futuresTradeInputsState); - const leverageSide = useRecoilValue(leverageSideState); - const position = useRecoilValue(positionState); + const { nativeSize, nativeSizeDelta } = useAppSelector(selectTradeSizeInputs); + const leverageSide = useAppSelector(selectLeverageSide); + const position = useAppSelector(selectPosition); const marketInfo = useAppSelector(selectMarketInfo); const marketAsset = useAppSelector(selectMarketAsset); - - const setConfirmationModalOpen = useSetRecoilState(confirmationModalOpenState); - - const { orderTxn } = useFuturesContext(); + const submitting = useAppSelector(selectIsModifyingIsolatedPosition); + const gasEstimate = useAppSelector(selectModifyIsolatedGasEstimate); const gasPrices = useMemo( () => (ethGasPriceQuery.isSuccess ? ethGasPriceQuery?.data ?? undefined : undefined), [ethGasPriceQuery.isSuccess, ethGasPriceQuery.data] ); - const transactionFee = estimateSnxTxGasCost(orderTxn); + useEffect(() => { + dispatch( + modifyIsolatedPositionEstimateGas({ + sizeDelta: nativeSizeDelta, + useNextPrice: true, + }) + ); + }, [nativeSizeDelta, dispatch]); + + const transactionFee = useMemo(() => gasEstimate?.cost ?? zeroBN, [gasEstimate?.cost]); const positionSize = position?.position?.size ?? zeroBN; @@ -127,13 +136,17 @@ const NextPriceConfirmationModal: FC = () => { ] ); - const onDismiss = () => { - setConfirmationModalOpen(false); - }; + const onDismiss = useCallback(() => { + dispatch(setOpenModal(null)); + }, [dispatch]); const handleConfirmOrder = async () => { - orderTxn.mutate(); - onDismiss(); + dispatch( + modifyIsolatedPosition({ + sizeDelta: nativeSizeDelta, + useNextPrice: true, + }) + ); }; return ( @@ -158,8 +171,12 @@ const NextPriceConfirmationModal: FC = () => { {t('futures.market.trade.confirmation.modal.max-leverage-disclaimer')} )} - - {t('futures.market.trade.confirmation.modal.confirm-order')} + + {submitting ? ( + + ) : ( + t('futures.market.trade.confirmation.modal.confirm-order') + )} @@ -169,8 +186,16 @@ const NextPriceConfirmationModal: FC = () => { items={dataRows} closeDrawer={onDismiss} buttons={ - - {t('futures.market.trade.confirmation.modal.confirm-order')} + + {submitting ? ( + + ) : ( + t('futures.market.trade.confirmation.modal.confirm-order') + )} } /> diff --git a/sections/futures/Trade/TradeConfirmationModal.tsx b/sections/futures/Trade/TradeConfirmationModal.tsx index 7d2f671cdd..380c4acc51 100644 --- a/sections/futures/Trade/TradeConfirmationModal.tsx +++ b/sections/futures/Trade/TradeConfirmationModal.tsx @@ -2,23 +2,23 @@ import Wei from '@synthetixio/wei'; import { capitalize } from 'lodash'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; import Button from 'components/Button'; import ErrorView from 'components/Error'; +import { ButtonLoader } from 'components/Loader/Loader'; import { DesktopOnlyView, MobileOrTabletView } from 'components/Media'; import { MIN_MARGIN_AMOUNT } from 'constants/futures'; -import { selectMarketAsset } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; import { - futuresOrderPriceState, - leverageSideState, - orderTypeState, - positionState, - potentialTradeDetailsState, -} from 'store/futures'; + selectCrossMarginOrderPrice, + selectLeverageSide, + selectMarketAsset, + selectOrderType, + selectPosition, + selectTradePreview, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; import { FlexDivCentered } from 'styles/common'; import { zeroBN, formatCurrency, formatDollars, formatNumber } from 'utils/formatters/number'; import { getDisplayAsset } from 'utils/futures'; @@ -31,6 +31,7 @@ type Props = { tradeFee: Wei; keeperFee?: Wei | null; errorMessage?: string | null | undefined; + isSubmitting?: boolean; onConfirmOrder: () => any; onDismiss: () => void; }; @@ -40,17 +41,18 @@ export default function TradeConfirmationModal({ gasFee, keeperFee, errorMessage, + isSubmitting, onConfirmOrder, onDismiss, }: Props) { const { t } = useTranslation(); const marketAsset = useAppSelector(selectMarketAsset); - const { data: potentialTradeDetails } = useRecoilValue(potentialTradeDetailsState); - const orderType = useRecoilValue(orderTypeState); - const orderPrice = useRecoilValue(futuresOrderPriceState); - const position = useRecoilValue(positionState); - const leverageSide = useRecoilValue(leverageSideState); + const potentialTradeDetails = useAppSelector(selectTradePreview); + const orderType = useAppSelector(selectOrderType); + const orderPrice = useAppSelector(selectCrossMarginOrderPrice); + const position = useAppSelector(selectPosition); + const leverageSide = useAppSelector(selectLeverageSide); const positionSide = useMemo(() => { if (potentialTradeDetails?.size.eq(zeroBN)) { @@ -156,9 +158,13 @@ export default function TradeConfirmationModal({ data-testid="trade-open-position-confirm-order-button" variant="flat" onClick={onConfirmOrder} - disabled={!positionDetails || !!disabledReason || gasFee?.eq(0)} + disabled={!positionDetails || isSubmitting || !!disabledReason} > - {disabledReason || t('futures.market.trade.confirmation.modal.confirm-order')} + {isSubmitting ? ( + + ) : ( + disabledReason || t('futures.market.trade.confirmation.modal.confirm-order') + )} {errorMessage && ( @@ -176,9 +182,13 @@ export default function TradeConfirmationModal({ - {disabledReason || t('futures.market.trade.confirmation.modal.confirm-order')} + {isSubmitting ? ( + + ) : ( + disabledReason || t('futures.market.trade.confirmation.modal.confirm-order') + )} } /> diff --git a/sections/futures/Trade/TradeConfirmationModalCrossMargin.tsx b/sections/futures/Trade/TradeConfirmationModalCrossMargin.tsx index 13c9823f8b..d6eb5a8ebc 100644 --- a/sections/futures/Trade/TradeConfirmationModalCrossMargin.tsx +++ b/sections/futures/Trade/TradeConfirmationModalCrossMargin.tsx @@ -2,7 +2,6 @@ import Wei from '@synthetixio/wei'; import { formatBytes32String } from 'ethers/lib/utils'; import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; import { DEFAULT_CROSSMARGIN_GAS_BUFFER_PCT } from 'constants/defaults'; import { useFuturesContext } from 'contexts/FuturesContext'; @@ -10,14 +9,14 @@ import { useRefetchContext } from 'contexts/RefetchContext'; import { monitorTransaction } from 'contexts/RelayerContext'; import useCrossMarginAccountContracts from 'hooks/useCrossMarginContracts'; import useEstimateGasCost from 'hooks/useEstimateGasCost'; -import { selectMarketKey } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { setOpenModal } from 'state/app/reducer'; import { - confirmationModalOpenState, - crossMarginMarginDeltaState, - futuresTradeInputsState, - isAdvancedOrderState, -} from 'store/futures'; + selectCrossMarginMarginDelta, + selectIsAdvancedOrder, + selectMarketKey, + selectTradeSizeInputs, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { isUserDeniedError } from 'utils/formatters/error'; import { zeroBN } from 'utils/formatters/number'; import logError from 'utils/logError'; @@ -26,19 +25,18 @@ import TradeConfirmationModal from './TradeConfirmationModal'; export default function TradeConfirmationModalCrossMargin() { const { t } = useTranslation(); - const { handleRefetch, refetchUntilUpdate } = useRefetchContext(); + const { handleRefetch } = useRefetchContext(); const { crossMarginAccountContract } = useCrossMarginAccountContracts(); const { estimateEthersContractTxCost } = useEstimateGasCost(); + const dispatch = useAppDispatch(); const marketKey = useAppSelector(selectMarketKey); - const crossMarginMarginDelta = useRecoilValue(crossMarginMarginDeltaState); - const tradeInputs = useRecoilValue(futuresTradeInputsState); - const isAdvancedOrder = useRecoilValue(isAdvancedOrderState); + const crossMarginMarginDelta = useAppSelector(selectCrossMarginMarginDelta); + const { nativeSizeDelta } = useAppSelector(selectTradeSizeInputs); + const isAdvancedOrder = useAppSelector(selectIsAdvancedOrder); const { submitCrossMarginOrder, resetTradeState, tradeFees } = useFuturesContext(); - const setConfirmationModalOpen = useSetRecoilState(confirmationModalOpenState); - const [error, setError] = useState(null); const [gasFee, setGasFee] = useState(null); const [gasLimit, setGasLimit] = useState(null); @@ -50,7 +48,7 @@ export default function TradeConfirmationModalCrossMargin() { { marketKey: formatBytes32String(marketKey), marginDelta: crossMarginMarginDelta.toBN(), - sizeDelta: tradeInputs.nativeSizeDelta.toBN(), + sizeDelta: nativeSizeDelta.toBN(), }, ]; const { gasPrice, gasLimit } = await estimateEthersContractTxCost( @@ -68,13 +66,13 @@ export default function TradeConfirmationModalCrossMargin() { crossMarginAccountContract, marketKey, crossMarginMarginDelta, - tradeInputs.nativeSizeDelta, + nativeSizeDelta, estimateEthersContractTxCost, ]); const onDismiss = useCallback(() => { - setConfirmationModalOpen(false); - }, [setConfirmationModalOpen]); + dispatch(setOpenModal(null)); + }, [dispatch]); const handleConfirmOrder = useCallback(async () => { setError(null); @@ -91,7 +89,7 @@ export default function TradeConfirmationModalCrossMargin() { onTxConfirmed: () => { resetTradeState(); handleRefetch('modify-position'); - refetchUntilUpdate('account-margin-change'); + handleRefetch('account-margin-change'); }, }); onDismiss(); @@ -102,16 +100,7 @@ export default function TradeConfirmationModalCrossMargin() { setError(t('common.transaction.transaction-failed')); } } - }, [ - gasLimit, - setError, - handleRefetch, - refetchUntilUpdate, - resetTradeState, - onDismiss, - submitCrossMarginOrder, - t, - ]); + }, [gasLimit, setError, handleRefetch, resetTradeState, onDismiss, submitCrossMarginOrder, t]); return ( gasEstimate?.cost ?? zeroBN, [gasEstimate?.cost]); - const onDismiss = () => { - setConfirmationModalOpen(false); - }; + useEffect(() => { + dispatch( + modifyIsolatedPositionEstimateGas({ + sizeDelta: nativeSizeDelta, + useNextPrice: false, + }) + ); + }, [nativeSizeDelta, dispatch]); + + const onDismiss = useCallback(() => { + dispatch(setOpenModal(null)); + }, [dispatch]); const handleConfirmOrder = async () => { - submitIsolatedMarginOrder(); - onDismiss(); + dispatch( + modifyIsolatedPosition({ + sizeDelta: nativeSizeDelta, + useNextPrice: false, + }) + ); }; return ( @@ -30,6 +50,7 @@ export default function TradeConfirmationModalIsolatedMargin() { onDismiss={onDismiss} onConfirmOrder={handleConfirmOrder} gasFee={transactionFee} + isSubmitting={submitting} tradeFee={potentialTradeDetails?.fee || zeroBN} /> ); diff --git a/sections/futures/Trade/TradeIsolatedMargin.tsx b/sections/futures/Trade/TradeIsolatedMargin.tsx index 86ca127b6f..f23abe0a7e 100644 --- a/sections/futures/Trade/TradeIsolatedMargin.tsx +++ b/sections/futures/Trade/TradeIsolatedMargin.tsx @@ -1,15 +1,13 @@ -import { useState } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; import styled from 'styled-components'; import SegmentedControl from 'components/SegmentedControl'; import { ISOLATED_MARGIN_ORDER_TYPES } from 'constants/futures'; -import { - setLeverageSide as setReduxLeverageSide, - setOrderType as setReduxOrderType, -} from 'state/futures/reducer'; -import { useAppDispatch } from 'state/hooks'; -import { balancesState, leverageSideState, orderTypeState, positionState } from 'store/futures'; +import { setOpenModal } from 'state/app/reducer'; +import { selectOpenModal } from 'state/app/selectors'; +import { changeLeverageSide } from 'state/futures/actions'; +import { setOrderType } from 'state/futures/reducer'; +import { selectLeverageSide, selectOrderType, selectPosition } from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { zeroBN } from 'utils/formatters/number'; import FeeInfoBox from '../FeeInfoBox'; @@ -27,19 +25,19 @@ type Props = { }; const TradeIsolatedMargin = ({ isMobile }: Props) => { - const [leverageSide, setLeverageSide] = useRecoilState(leverageSideState); - const { susdWalletBalance } = useRecoilValue(balancesState); - const position = useRecoilValue(positionState); + const dispatch = useAppDispatch(); + + const leverageSide = useAppSelector(selectLeverageSide); + const position = useAppSelector(selectPosition); + const openModal = useAppSelector(selectOpenModal); - const [orderType, setOrderType] = useRecoilState(orderTypeState); - const [openTransferModal, setOpenTransferModal] = useState(false); + const orderType = useAppSelector(selectOrderType); const totalMargin = position?.remainingMargin ?? zeroBN; - const dispatch = useAppDispatch(); return (
setOpenTransferModal(true)} + onManageBalance={() => dispatch(setOpenModal('futures_isolated_transfer'))} balance={totalMargin} accountType={'isolated_margin'} /> @@ -51,8 +49,7 @@ const TradeIsolatedMargin = ({ isMobile }: Props) => { values={ISOLATED_MARGIN_ORDER_TYPES} selectedIndex={ISOLATED_MARGIN_ORDER_TYPES.indexOf(orderType)} onChange={(oType: number) => { - setOrderType(oType === 0 ? 'market' : 'next price'); - dispatch(setReduxOrderType(oType === 0 ? 'market' : 'next price')); + dispatch(setOrderType(oType === 0 ? 'market' : 'next price')); }} /> @@ -61,8 +58,7 @@ const TradeIsolatedMargin = ({ isMobile }: Props) => { { - setLeverageSide(side); - dispatch(setReduxLeverageSide(side)); + dispatch(changeLeverageSide(side)); }} /> @@ -73,11 +69,10 @@ const TradeIsolatedMargin = ({ isMobile }: Props) => { - {openTransferModal && ( + {openModal === 'futures_isolated_transfer' && ( setOpenTransferModal(false)} + onDismiss={() => dispatch(setOpenModal(null))} /> )}
diff --git a/sections/futures/Trade/TransferIsolatedMarginModal.tsx b/sections/futures/Trade/TransferIsolatedMarginModal.tsx index 47ed30bdb6..c906ec84bc 100644 --- a/sections/futures/Trade/TransferIsolatedMarginModal.tsx +++ b/sections/futures/Trade/TransferIsolatedMarginModal.tsx @@ -1,8 +1,6 @@ -import useSynthetixQueries from '@synthetixio/queries'; -import Wei, { wei } from '@synthetixio/wei'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { wei } from '@synthetixio/wei'; +import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; @@ -12,34 +10,32 @@ import CustomInput from 'components/Input/CustomInput'; import SegmentedControl from 'components/SegmentedControl'; import Spacer from 'components/Spacer'; import { MIN_MARGIN_AMOUNT } from 'constants/futures'; -import { NO_VALUE } from 'constants/placeholder'; -import { useRefetchContext } from 'contexts/RefetchContext'; -import { monitorTransaction } from 'contexts/RelayerContext'; -import useEstimateGasCost from 'hooks/useEstimateGasCost'; -import { selectMarketAsset } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { positionState } from 'store/futures'; -import { gasSpeedState } from 'store/wallet'; +import { selectSusdBalance } from 'state/balances/selectors'; +import { depositIsolatedMargin, withdrawIsolatedMargin } from 'state/futures/actions'; +import { + selectIsolatedTransferError, + selectIsSubmittingIsolatedTransfer, + selectPosition, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { FlexDivRowCentered } from 'styles/common'; import { formatDollars, zeroBN } from 'utils/formatters/number'; -import { getDisplayAsset } from 'utils/futures'; type Props = { onDismiss(): void; defaultTab: 'deposit' | 'withdraw'; - sUSDBalance: Wei; }; const PLACEHOLDER = '$0.00'; -const TransferIsolatedMarginModal: React.FC = ({ onDismiss, sUSDBalance, defaultTab }) => { +const TransferIsolatedMarginModal: React.FC = ({ onDismiss, defaultTab }) => { const { t } = useTranslation(); - const { useEthGasPriceQuery, useSynthetixTxn } = useSynthetixQueries(); - const { estimateSnxTxGasCost } = useEstimateGasCost(); + const dispatch = useAppDispatch(); - const gasSpeed = useRecoilValue(gasSpeedState); - const position = useRecoilValue(positionState); - const marketAsset = useAppSelector(selectMarketAsset); + const position = useAppSelector(selectPosition); + const submitting = useAppSelector(selectIsSubmittingIsolatedTransfer); + const txError = useAppSelector(selectIsolatedTransferError); + const susdBalance = useAppSelector(selectSusdBalance); const minDeposit = useMemo(() => { const accessibleMargin = position?.accessibleMargin ?? zeroBN; @@ -50,17 +46,13 @@ const TransferIsolatedMarginModal: React.FC = ({ onDismiss, sUSDBalance, const [amount, setAmount] = useState(''); const [transferType, setTransferType] = useState(defaultTab === 'deposit' ? 0 : 1); - const ethGasPriceQuery = useEthGasPriceQuery(); - const { handleRefetch } = useRefetchContext(); - const gasPrice = ethGasPriceQuery.data != null ? ethGasPriceQuery.data[gasSpeed] : null; - - const susdBal = transferType === 0 ? sUSDBalance : position?.accessibleMargin || zeroBN; + const susdBal = transferType === 0 ? susdBalance : position?.accessibleMargin || zeroBN; const accessibleMargin = useMemo(() => position?.accessibleMargin ?? zeroBN, [ position?.accessibleMargin, ]); const isDisabled = useMemo(() => { - if (!amount) { + if (!amount || submitting) { return true; } const amtWei = wei(amount); @@ -68,49 +60,13 @@ const TransferIsolatedMarginModal: React.FC = ({ onDismiss, sUSDBalance, return true; } return false; - }, [amount, susdBal, minDeposit, transferType]); + }, [amount, susdBal, minDeposit, transferType, submitting]); const computedWithdrawAmount = useMemo( - () => - accessibleMargin.eq(wei(amount || 0)) - ? accessibleMargin.mul(wei(-1)).toBN() - : wei(-amount).toBN(), + () => (accessibleMargin.eq(wei(amount || 0)) ? accessibleMargin : wei(amount || 0)), [amount, accessibleMargin] ); - const depositTxn = useSynthetixTxn( - `FuturesMarket${getDisplayAsset(marketAsset)}`, - 'transferMargin', - [wei(amount || 0).toBN()], - gasPrice || undefined, - { enabled: !!marketAsset && !!amount && !isDisabled && transferType === 0 } - ); - - const withdrawTxn = useSynthetixTxn( - `FuturesMarket${getDisplayAsset(marketAsset)}`, - 'transferMargin', - [computedWithdrawAmount], - gasPrice || undefined, - { enabled: !!marketAsset && !!amount && transferType === 1 } - ); - - const transactionFee = estimateSnxTxGasCost(transferType === 0 ? depositTxn : withdrawTxn); - - useEffect(() => { - const hash = depositTxn.hash ?? withdrawTxn.hash; - if (hash) { - monitorTransaction({ - txHash: hash, - onTxConfirmed: () => { - handleRefetch('margin-change'); - onDismiss(); - }, - }); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [depositTxn.hash, withdrawTxn.hash]); - const handleSetMax = useCallback(() => { if (transferType === 0) { setAmount(susdBal.toString()); @@ -124,6 +80,14 @@ const TransferIsolatedMarginModal: React.FC = ({ onDismiss, sUSDBalance, setAmount(''); }; + const onDeposit = () => { + dispatch(depositIsolatedMargin(wei(amount))); + }; + + const onWithdraw = () => { + dispatch(withdrawIsolatedMargin(computedWithdrawAmount)); + }; + return ( = ({ onDismiss, sUSDBalance, data-testid="futures-market-trade-deposit-margin-button" disabled={isDisabled} fullWidth - onClick={transferType === 0 ? () => depositTxn.mutate() : () => withdrawTxn.mutate()} + onClick={transferType === 0 ? onDeposit : onWithdraw} > {transferType === 0 ? t('futures.market.trade.margin.modal.deposit.button') : t('futures.market.trade.margin.modal.withdraw.button')} - - {t('futures.market.trade.margin.modal.gas-fee')}: - - - {transactionFee ? formatDollars(transactionFee, { maxDecimals: 1 }) : NO_VALUE} - - - - - {depositTxn.errorMessage && } + {txError && } ); }; diff --git a/sections/futures/TradeCrossMargin/CrossMarginInfoBox.tsx b/sections/futures/TradeCrossMargin/CrossMarginInfoBox.tsx index fe600ddfdc..1c16051741 100644 --- a/sections/futures/TradeCrossMargin/CrossMarginInfoBox.tsx +++ b/sections/futures/TradeCrossMargin/CrossMarginInfoBox.tsx @@ -1,6 +1,5 @@ import Wei, { wei } from '@synthetixio/wei'; import React, { useCallback, useMemo, useState } from 'react'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import WithdrawArrow from 'assets/svg/futures/withdraw-arrow.svg'; @@ -8,19 +7,21 @@ import InfoBox from 'components/InfoBox'; import { MiniLoader } from 'components/Loader'; import PreviewArrow from 'components/PreviewArrow'; import { useFuturesContext } from 'contexts/FuturesContext'; -import { FuturesPotentialTradeDetails } from 'queries/futures/types'; -import { selectMarketInfo } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; +import { FuturesPotentialTradeDetails } from 'sdk/types/futures'; import { - crossMarginMarginDeltaState, - positionState, - potentialTradeDetailsState, - tradeFeesState, - futuresTradeInputsState, - orderTypeState, - futuresOrderPriceState, - crossMarginAccountOverviewState, -} from 'store/futures'; + selectCrossMarginBalanceInfo, + selectCrossMarginMarginDelta, + selectCrossMarginOrderPrice, + selectCrossMarginTradeFees, + selectMarketInfo, + selectOrderType, + selectPosition, + selectTradePreview, + selectTradePreviewStatus, + selectTradeSizeInputs, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; +import { FetchStatus } from 'state/types'; import { PillButtonSpan } from 'styles/common'; import { formatCurrency, @@ -30,7 +31,7 @@ import { zeroBN, } from 'utils/formatters/number'; -import EditLeverageModal from './EditLeverageModal'; +import EditLeverageModal from './EditCrossMarginLeverageModal'; import ManageKeeperBalanceModal from './ManageKeeperBalanceModal'; type Props = { @@ -40,17 +41,18 @@ type Props = { function MarginInfoBox({ editingLeverage }: Props) { const { selectedLeverage } = useFuturesContext(); - const position = useRecoilValue(positionState); + const position = useAppSelector(selectPosition); const marketInfo = useAppSelector(selectMarketInfo); - const { nativeSize } = useRecoilValue(futuresTradeInputsState); - const potentialTrade = useRecoilValue(potentialTradeDetailsState); - const marginDelta = useRecoilValue(crossMarginMarginDeltaState); - const { freeMargin: crossMarginFreeMargin, keeperEthBal } = useRecoilValue( - crossMarginAccountOverviewState + const { nativeSize } = useAppSelector(selectTradeSizeInputs); + const potentialTrade = useAppSelector(selectTradePreview); + const marginDelta = useAppSelector(selectCrossMarginMarginDelta); + const { freeMargin: crossMarginFreeMargin, keeperEthBal } = useAppSelector( + selectCrossMarginBalanceInfo ); - const orderType = useRecoilValue(orderTypeState); - const orderPrice = useRecoilValue(futuresOrderPriceState); - const { crossMarginFee } = useRecoilValue(tradeFeesState); + const previewStatus = useAppSelector(selectTradePreviewStatus); + const orderType = useAppSelector(selectOrderType); + const orderPrice = useAppSelector(selectCrossMarginOrderPrice); + const { crossMarginFee } = useAppSelector(selectCrossMarginTradeFees); const [openModal, setOpenModal] = useState<'leverage' | 'keeper-deposit' | null>(null); @@ -58,11 +60,13 @@ function MarginInfoBox({ editingLeverage }: Props) { const remainingMargin = position?.remainingMargin ?? zeroBN; const marginUsage = totalMargin.gt(zeroBN) ? remainingMargin.div(totalMargin) : zeroBN; - + const minInitialMargin = useMemo(() => marketInfo?.minInitialMargin ?? zeroBN, [ + marketInfo?.minInitialMargin, + ]); const previewTotalMargin = useMemo(() => { const remainingMargin = crossMarginFreeMargin.sub(marginDelta); - return remainingMargin.add(potentialTrade.data?.margin || zeroBN); - }, [crossMarginFreeMargin, marginDelta, potentialTrade.data?.margin]); + return remainingMargin.add(potentialTrade?.margin || zeroBN); + }, [crossMarginFreeMargin, marginDelta, potentialTrade?.margin]); const getPotentialAvailableMargin = useCallback( (previewTrade: FuturesPotentialTradeDetails | null, marketMaxLeverage: Wei | undefined) => { @@ -74,8 +78,8 @@ function MarginInfoBox({ editingLeverage }: Props) { // If the user has a position open, we'll enforce a min initial margin requirement. if (inaccessible.gt(0)) { - if (inaccessible.lt(previewTrade?.minInitialMargin ?? zeroBN)) { - inaccessible = previewTrade?.minInitialMargin ?? zeroBN; + if (inaccessible.lt(minInitialMargin)) { + inaccessible = minInitialMargin; } } @@ -84,23 +88,23 @@ function MarginInfoBox({ editingLeverage }: Props) { ? previewTotalMargin.sub(inaccessible).abs() : zeroBN; }, - [previewTotalMargin] + [previewTotalMargin, minInitialMargin] ); const previewAvailableMargin = React.useMemo(() => { const potentialAvailableMargin = getPotentialAvailableMargin( - potentialTrade.data, + potentialTrade, marketInfo?.maxLeverage ); return potentialAvailableMargin; - }, [potentialTrade.data, marketInfo?.maxLeverage, getPotentialAvailableMargin]); + }, [potentialTrade, marketInfo?.maxLeverage, getPotentialAvailableMargin]); const potentialMarginUsage = useMemo(() => { - if (!potentialTrade.data) return zeroBN; - const notionalValue = potentialTrade.data.notionalValue.abs(); - const maxSize = totalMargin.mul(potentialTrade.data.leverage); + if (!potentialTrade) return zeroBN; + const notionalValue = potentialTrade.notionalValue.abs(); + const maxSize = totalMargin.mul(potentialTrade.leverage); return maxSize.gt(0) ? notionalValue.div(maxSize) : zeroBN; - }, [potentialTrade.data, totalMargin]); + }, [potentialTrade, totalMargin]); const previewTradeData = React.useMemo(() => { const size = wei(nativeSize || zeroBN); @@ -110,12 +114,12 @@ function MarginInfoBox({ editingLeverage }: Props) { ((orderType === 'market' || orderType === 'next price') && (!size.eq(0) || !marginDelta.eq(0))) || ((orderType === 'limit' || orderType === 'stop market') && !!orderPrice && !size.eq(0)), - totalMargin: potentialTrade.data?.margin.sub(crossMarginFee) || zeroBN, + totalMargin: potentialTrade?.margin.sub(crossMarginFee) || zeroBN, freeAccountMargin: crossMarginFreeMargin.sub(marginDelta), availableMargin: previewAvailableMargin.gt(0) ? previewAvailableMargin : zeroBN, - size: potentialTrade.data?.size || zeroBN, - leverage: potentialTrade.data?.margin.gt(0) - ? potentialTrade.data.notionalValue.div(potentialTrade.data.margin).abs() + size: potentialTrade?.size || zeroBN, + leverage: potentialTrade?.margin.gt(0) + ? potentialTrade.notionalValue.div(potentialTrade.margin).abs() : zeroBN, marginUsage: potentialMarginUsage.gt(1) ? wei(1) : potentialMarginUsage, }; @@ -125,16 +129,17 @@ function MarginInfoBox({ editingLeverage }: Props) { crossMarginFee, orderType, orderPrice, - potentialTrade.data?.margin, + potentialTrade?.margin, previewAvailableMargin, - potentialTrade.data?.notionalValue, - potentialTrade.data?.size, + potentialTrade?.notionalValue, + potentialTrade?.size, crossMarginFreeMargin, potentialMarginUsage, ]); - const showPreview = previewTradeData.showPreview && !potentialTrade.data?.showStatus; + const showPreview = previewTradeData.showPreview && !potentialTrade?.showStatus; + const isLoading = previewStatus.status === FetchStatus.Loading; return ( <> - {potentialTrade.status === 'fetching' ? ( - - ) : ( - formatDollars(previewTradeData.freeAccountMargin) - )} + {isLoading ? : formatDollars(previewTradeData.freeAccountMargin)}
), } @@ -161,11 +162,7 @@ function MarginInfoBox({ editingLeverage }: Props) { value: formatDollars(position?.remainingMargin || 0), valueNode: ( - {potentialTrade.status === 'fetching' ? ( - - ) : ( - formatDollars(previewTradeData.totalMargin) - )} + {isLoading ? : formatDollars(previewTradeData.totalMargin)} ), }, @@ -173,11 +170,7 @@ function MarginInfoBox({ editingLeverage }: Props) { value: formatPercent(marginUsage), valueNode: ( - {potentialTrade.status === 'fetching' ? ( - - ) : ( - formatPercent(previewTradeData?.marginUsage) - )} + {isLoading ? : formatPercent(previewTradeData?.marginUsage)} ), }, @@ -217,11 +210,7 @@ function MarginInfoBox({ editingLeverage }: Props) { ), valueNode: ( - {potentialTrade.status === 'fetching' ? ( - - ) : ( - formatNumber(previewTradeData.leverage || 0) + 'x' - )} + {isLoading ? : formatNumber(previewTradeData.leverage || 0) + 'x'} ), }, @@ -230,7 +219,7 @@ function MarginInfoBox({ editingLeverage }: Props) { /> {openModal === 'leverage' && ( - setOpenModal(null)} /> + setOpenModal(null)} /> )} {openModal === 'keeper-deposit' && ( setOpenModal(null)} /> diff --git a/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx b/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx index 265eb5ddb4..871b836193 100644 --- a/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx +++ b/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx @@ -1,8 +1,6 @@ import { wei } from '@synthetixio/wei'; -import { constants } from 'ethers'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; @@ -12,12 +10,15 @@ import CustomInput from 'components/Input/CustomInput'; import Loader from 'components/Loader'; import SegmentedControl from 'components/SegmentedControl'; import { MIN_MARGIN_AMOUNT } from 'constants/futures'; -import Connector from 'containers/Connector'; -import { useRefetchContext } from 'contexts/RefetchContext'; -import { monitorTransaction } from 'contexts/RelayerContext'; -import useCrossMarginAccountContracts from 'hooks/useCrossMarginContracts'; -import useSUSDContract from 'hooks/useSUSDContract'; -import { balancesState, crossMarginAccountOverviewState } from 'store/futures'; +import { selectBalances } from 'state/balances/selectors'; +import { approveCrossMargin, depositCrossMargin, withdrawCrossMargin } from 'state/futures/actions'; +import { + selectCrossMarginBalanceInfo, + selectFuturesTransaction, + selectIsApprovingCrossDeposit, + selectIsSubmittingCrossTransfer, +} from 'state/futures/selectors'; +import { useAppDispatch, useAppSelector } from 'state/hooks'; import { FlexDivRowCentered } from 'styles/common'; import { formatDollars, zeroBN } from 'utils/formatters/number'; import logError from 'utils/logError'; @@ -25,7 +26,6 @@ import logError from 'utils/logError'; type DepositMarginModalProps = { defaultTab: 'deposit' | 'withdraw'; onDismiss(): void; - onComplete?(): void; }; const PLACEHOLDER = '$0.00'; @@ -33,118 +33,64 @@ const PLACEHOLDER = '$0.00'; export default function DepositWithdrawCrossMargin({ defaultTab = 'deposit', onDismiss, - onComplete, }: DepositMarginModalProps) { const { t } = useTranslation(); - const { signer } = Connector.useContainer(); - const { crossMarginAccountContract } = useCrossMarginAccountContracts(); - const { refetchUntilUpdate } = useRefetchContext(); - const susdContract = useSUSDContract(); + const dispatch = useAppDispatch(); - const balances = useRecoilValue(balancesState); - const { freeMargin, allowance } = useRecoilValue(crossMarginAccountOverviewState); + const balances = useAppSelector(selectBalances); + const crossMarginBalanceInfo = useAppSelector(selectCrossMarginBalanceInfo); + const transactionState = useAppSelector(selectFuturesTransaction); + const isSubmitting = useAppSelector(selectIsSubmittingCrossTransfer); + const isApproving = useAppSelector(selectIsApprovingCrossDeposit); const [amount, setAmount] = useState(''); const [transferType, setTransferType] = useState(0); - const [txState, setTxState] = useState<'none' | 'approving' | 'submitting' | 'complete'>('none'); - const [error, setError] = useState(null); useEffect(() => { setTransferType(defaultTab === 'deposit' ? 0 : 1); }, [defaultTab]); - const susdBal = transferType === 0 ? balances?.susdWalletBalance || zeroBN : freeMargin; + const susdBal = + transferType === 0 ? balances?.susdWalletBalance || zeroBN : crossMarginBalanceInfo.freeMargin; const submitDeposit = useCallback(async () => { - try { - if (!crossMarginAccountContract) throw new Error('No cross-margin account'); - - setTxState('submitting'); - const tx = await crossMarginAccountContract.deposit(wei(amount || 0).toBN()); - monitorTransaction({ - txHash: tx.hash, - onTxConfirmed: async () => { - await refetchUntilUpdate('account-margin-change'); - setTxState('complete'); - onComplete?.(); - onDismiss(); - }, - }); - } catch (err) { - setError(err.message); - setTxState('none'); - logError(err); - } - }, [crossMarginAccountContract, amount, refetchUntilUpdate, onComplete, onDismiss]); + dispatch(depositCrossMargin(wei(amount))); + }, [amount, dispatch]); const depositMargin = useCallback(async () => { try { - const wallet = await signer?.getAddress(); - - if (!crossMarginAccountContract || !wallet) throw new Error('No cross margin account'); const weiAmount = wei(amount ?? 0, 18); - if (wei(allowance).lt(weiAmount)) { - setTxState('approving'); - const tx = await susdContract?.approve( - crossMarginAccountContract.address, - constants.MaxUint256 - ); - if (tx?.hash) { - monitorTransaction({ - txHash: tx.hash, - onTxConfirmed: () => { - submitDeposit(); - }, - }); - } + if (wei(crossMarginBalanceInfo.allowance).lt(weiAmount)) { + dispatch(approveCrossMargin()); } else { submitDeposit(); } } catch (err) { - setError(err.message); - setTxState('none'); logError(err); } - }, [crossMarginAccountContract, amount, signer, susdContract, allowance, submitDeposit]); + }, [amount, crossMarginBalanceInfo.allowance, dispatch, submitDeposit]); const withdrawMargin = useCallback(async () => { - try { - if (!crossMarginAccountContract) throw new Error('No cross-margin account'); - setTxState('submitting'); - const tx = await crossMarginAccountContract.withdraw(wei(amount).toBN()); - monitorTransaction({ - txHash: tx.hash, - onTxConfirmed: async () => { - await refetchUntilUpdate('account-margin-change'); - setTxState('complete'); - onComplete?.(); - onDismiss(); - }, - }); - } catch (err) { - setError(err.message); - setTxState('none'); - logError(err); - } - }, [crossMarginAccountContract, amount, refetchUntilUpdate, onComplete, onDismiss]); + dispatch(withdrawCrossMargin(wei(amount))); + }, [amount, dispatch]); const disabledReason = useMemo(() => { const amtWei = wei(amount || 0); if (transferType === 0) { - const total = wei(freeMargin).add(amtWei); + const total = wei(crossMarginBalanceInfo.freeMargin).add(amtWei); if (total.lt(MIN_MARGIN_AMOUNT)) return t('futures.market.trade.margin.modal.deposit.min-deposit'); if (amtWei.gt(susdBal)) return t('futures.market.trade.margin.modal.deposit.exceeds-balance'); } else { - if (amtWei.gt(freeMargin)) + if (amtWei.gt(crossMarginBalanceInfo.freeMargin)) return t('futures.market.trade.margin.modal.deposit.exceeds-balance'); } - }, [amount, freeMargin, transferType, susdBal, t]); + }, [amount, crossMarginBalanceInfo.freeMargin, transferType, susdBal, t]); const isApproved = useMemo(() => { - return allowance.gt(wei(amount || 0)); - }, [allowance, amount]); + return crossMarginBalanceInfo.allowance.gt(wei(amount || 0)); + }, [crossMarginBalanceInfo.allowance, amount]); const handleSetMax = React.useCallback(() => { setAmount(susdBal.toString()); @@ -155,6 +101,8 @@ export default function DepositWithdrawCrossMargin({ setAmount(''); }; + const isLoading = isSubmitting || isApproving; + return ( - {txState === 'approving' || txState === 'submitting' ? ( + {isLoading ? ( ) : ( disabledReason || @@ -206,7 +154,7 @@ export default function DepositWithdrawCrossMargin({ )} - {error && } + {transactionState?.error && } ); } diff --git a/sections/futures/TradeCrossMargin/EditLeverageModal.tsx b/sections/futures/TradeCrossMargin/EditCrossMarginLeverageModal.tsx similarity index 81% rename from sections/futures/TradeCrossMargin/EditLeverageModal.tsx rename to sections/futures/TradeCrossMargin/EditCrossMarginLeverageModal.tsx index 9f4311d6c5..03c3ba73bc 100644 --- a/sections/futures/TradeCrossMargin/EditLeverageModal.tsx +++ b/sections/futures/TradeCrossMargin/EditCrossMarginLeverageModal.tsx @@ -2,7 +2,6 @@ import { wei } from '@synthetixio/wei'; import { debounce } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRecoilState, useRecoilValue } from 'recoil'; import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; @@ -16,22 +15,23 @@ import { DEFAULT_LEVERAGE } from 'constants/defaults'; import { useFuturesContext } from 'contexts/FuturesContext'; import { useRefetchContext } from 'contexts/RefetchContext'; import { monitorTransaction } from 'contexts/RelayerContext'; -import usePersistedRecoilState from 'hooks/usePersistedRecoilState'; import { ORDER_PREVIEW_ERRORS_I18N, previewErrorI18n } from 'queries/futures/constants'; -import { setOrderType as setReduxOrderType } from 'state/futures/reducer'; -import { selectMarketAsset, selectMarketInfo } from 'state/futures/selectors'; -import { useAppSelector, useAppDispatch } from 'state/hooks'; +import { editExistingPositionLeverage, editCrossMarginSize } from 'state/futures/actions'; +import { setCrossMarginLeverage, setOrderType as setReduxOrderType } from 'state/futures/reducer'; import { - crossMarginTotalMarginState, - orderTypeState, - positionState, - potentialTradeDetailsState, - preferredLeverageState, - tradeFeesState, -} from 'store/futures'; + selectCrossMarginBalanceInfo, + selectCrossMarginSelectedLeverage, + selectCrossMarginTradeFees, + selectMarketInfo, + selectOrderType, + selectPosition, + selectTradePreview, + selectTradePreviewError, +} from 'state/futures/selectors'; +import { useAppSelector, useAppDispatch } from 'state/hooks'; import { FlexDivRow, FlexDivRowCentered } from 'styles/common'; import { isUserDeniedError } from 'utils/formatters/error'; -import { formatDollars } from 'utils/formatters/number'; +import { formatDollars, zeroBN } from 'utils/formatters/number'; import logError from 'utils/logError'; import FeeInfoBox from '../FeeInfoBox'; @@ -40,29 +40,31 @@ import MarginInfoBox from './CrossMarginInfoBox'; type DepositMarginModalProps = { onDismiss(): void; - editMode: 'existing_position' | 'next_trade'; + editMode: 'existing_position' | 'new_position'; }; export default function EditLeverageModal({ onDismiss, editMode }: DepositMarginModalProps) { const { t } = useTranslation(); - const { handleRefetch, refetchUntilUpdate } = useRefetchContext(); - const { - selectedLeverage, - onLeverageChange, - resetTradeState, - submitCrossMarginOrder, - onChangeOpenPosLeverage, - } = useFuturesContext(); + const { handleRefetch } = useRefetchContext(); + const dispatch = useAppDispatch(); + const { resetTradeState, submitCrossMarginOrder } = useFuturesContext(); - const marketAsset = useAppSelector(selectMarketAsset); - const market = useAppSelector(selectMarketInfo); - const position = useRecoilValue(positionState); - const totalMargin = useRecoilValue(crossMarginTotalMarginState); - const tradeFees = useRecoilValue(tradeFeesState); - const { error: previewError, data: previewData } = useRecoilValue(potentialTradeDetailsState); - const [orderType, setOrderType] = useRecoilState(orderTypeState); + const onLeverageChange = useCallback( + (leverage: number) => { + dispatch(setCrossMarginLeverage(String(leverage))); + dispatch(editCrossMarginSize('', 'usd')); + }, + [dispatch] + ); - const [preferredLeverage, setPreferredLeverage] = usePersistedRecoilState(preferredLeverageState); + const balanceInfo = useAppSelector(selectCrossMarginBalanceInfo); + const market = useAppSelector(selectMarketInfo); + const position = useAppSelector(selectPosition); + const tradeFees = useAppSelector(selectCrossMarginTradeFees); + const previewData = useAppSelector(selectTradePreview); + const previewError = useAppSelector(selectTradePreviewError); + const orderType = useAppSelector(selectOrderType); + const selectedLeverage = useAppSelector(selectCrossMarginSelectedLeverage); const [leverage, setLeverage] = useState( editMode === 'existing_position' && position?.position @@ -71,13 +73,15 @@ export default function EditLeverageModal({ onDismiss, editMode }: DepositMargin ); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); - const dispatch = useAppDispatch(); + + const totalMargin = useMemo(() => { + return position?.remainingMargin.add(balanceInfo.freeMargin) ?? zeroBN; + }, [position?.remainingMargin, balanceInfo.freeMargin]); const maxLeverage = Number((market?.maxLeverage || wei(DEFAULT_LEVERAGE)).toString(2)); useEffect(() => { if (editMode === 'existing_position' && orderType !== 'market') { - setOrderType('market'); dispatch(setReduxOrderType('market')); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -106,7 +110,7 @@ export default function EditLeverageModal({ onDismiss, editMode }: DepositMargin debounce((leverage: number) => { if (leverage >= 1) { editMode === 'existing_position' - ? onChangeOpenPosLeverage(leverage) + ? dispatch(editExistingPositionLeverage(String(leverage))) : onLeverageChange(leverage); } }, 200), @@ -129,7 +133,7 @@ export default function EditLeverageModal({ onDismiss, editMode }: DepositMargin try { resetTradeState(); handleRefetch('modify-position'); - refetchUntilUpdate('account-margin-change'); + handleRefetch('account-margin-change'); setSubmitting(false); onDismiss(); } catch (err) { @@ -145,29 +149,24 @@ export default function EditLeverageModal({ onDismiss, editMode }: DepositMargin } resetTradeState(); } else { + // TODO: consolidate leverage states onLeverageChange(leverage); - setPreferredLeverage({ - ...preferredLeverage, - [marketAsset]: String(leverage), - }); + dispatch(setCrossMarginLeverage(String(leverage))); onDismiss(); } }, [ - marketAsset, leverage, position?.position, - preferredLeverage, editMode, setSubmitting, resetTradeState, t, - setPreferredLeverage, onLeverageChange, submitCrossMarginOrder, setError, - refetchUntilUpdate, handleRefetch, onDismiss, + dispatch, ]); const onClose = () => { @@ -227,7 +226,7 @@ export default function EditLeverageModal({ onDismiss, editMode }: DepositMargin - {editMode === 'next_trade' && ( + {editMode === 'new_position' && (