Skip to content

Commit

Permalink
feat: wire-up rfox staking (#6925)
Browse files Browse the repository at this point in the history
* staking components

* add staking components

* fix translation

* move files around

* Revert "move files around"

This reverts commit b8b7948.

* revert

* Revert "revert"

This reverts commit 8a47fd3.

* idfk

* rename folders

* remove the older folders

* Update StakeInput.tsx

* translations

* use already defined translations

* update others

* Update AddressSelection.tsx

* [skip ci] wip: wire-up rfox staking

* feat: don't display gas row when approval required

* [skip ci] feat: more todo

* [skip ci] fix: derp

* [skip ci] fix: forgotten mergefix

* [skip ci] fix: and another one

* feat: abi types inference in wagmi

* feat: disable when no runeAddress

* feat: add thorchain validateAddress

* feat: wire-up validation

* feat: approve

* feat: actual approve

* fix: i'm derp

* feat: she works

* fix: types

* feat: use constant

* feat: more cleanup

* feat: tackle many TODOs

* fix: generatedAssetData.json workaround

* feat: regen asset data

* feat: implement form validation in TradeAmountInput

* feat: regen assetData

* feat: fox on arb market-data monkey patch

* feat: monkey-patch for now vs. regenerating all

* feat: nullish access of control when in the context of an undefined form
context

* fix: shit

* feat: input display either approval or actual stake fees

* feat: improve loading stateroos

* feat: don't fire queries when amount invalid

* feat: improve loading states some more

* feat: share of pool percentage

* feat: cleanup useless mutation

* feat: actually revert mutation to benefit from loading states

This reverts commit 8f3d830.

* feat: leverage form some more

* feat: bump viem and wagmi to latest

* feat: move stakeFees to getFees

* feat: move getFees into react-queries

* fix: wallet doesn't serialize in JSON derp

* fix: types

* feat: pass validation down

* feat: last cleanup

* feat: share of pool at confirm step

* feat: cleanup TODO

* feat: remove comment

* fix: only show recent transactions from connected wallet (#6938)

* chore: use effective coalesing for selector cache keys and download button (#6936)

chore: use effective coalesing

* feat: form-based fee balance validation

* feat: invalidate queries on from change

* fix: user share of pool

* fix: user share of pool

* feat: rm todo

* feat: cherry-pick renaming bits from unstaking PR

* feat: cherry-pick cleanup bits from unstaking PR

* fix: types

* fix: types after mergefix

* feat: fallback controller to unrug change address current view-layer

* fix: hydrate FOX market data in StakingInput

* feat: pass cryptoAmount in too in TradeAmountInput

* fix: tradeamountinput add missing prefix for crypto as opposite currency when `isFiat`

* fix: missing interpolation in translation

* fix: use stakingInfo vs. balanceOf for user balanceOf

* feat: rm debug logs

* feat: add toast

* feat: add isTransitioning

* feat: add base to wagmi-config

* feat: clearer naming

* feat: conventional event-handler naming

* feat: omit undefined param in sendStakeTx and rename to handleStake

---------

Co-authored-by: reallybeard <[email protected]>
Co-authored-by: kevin <[email protected]>
Co-authored-by: woody <[email protected]>
  • Loading branch information
4 people authored May 24, 2024
1 parent eb88ad2 commit e396ffb
Show file tree
Hide file tree
Showing 31 changed files with 3,010 additions and 302 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
"eth-url-parser": "^1.0.4",
"ethers": "^6.11.1",
"ethers5": "npm:[email protected]",
"eventemitter2": "5.0.1",
"framer-motion": "^11.0.3",
"friendly-challenge": "0.9.2",
"grapheme-splitter": "^1.0.4",
Expand Down Expand Up @@ -206,7 +207,8 @@
"styled-components": "^6.0.7",
"uuid": "^9.0.0",
"vaul": "^0.9.0",
"viem": "^1.16.6",
"viem": "^2.10.9",
"wagmi": "^2.9.2",
"web-vitals": "^2.1.4"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions packages/caip/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const baseAssetId: AssetId = 'eip155:8453/slip44:60'

export const foxOnGnosisAssetId: AssetId =
'eip155:100/erc20:0x21a42669643f45bc0e086b8fc2ed70c23d67509d'
export const foxOnArbitrumOneAssetId: AssetId =
'eip155:42161/erc20:0xf929de51d91c77e42f5090069e0ad7a09e513c73'
export const foxAssetId: AssetId = 'eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d'
export const foxatarAssetId: AssetId =
'eip155:137/erc721:0x2e727c425a11ce6b8819b3004db332c12d2af2a2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { supportsThorchain } from '@shapeshiftoss/hdwallet-core'
import type { BIP44Params } from '@shapeshiftoss/types'
import { KnownChainIds } from '@shapeshiftoss/types'
import * as unchained from '@shapeshiftoss/unchained-client'
import { bech32 } from 'bech32'

import { ErrorHandler } from '../../error/ErrorHandler'
import type {
Expand All @@ -16,8 +17,9 @@ import type {
GetFeeDataInput,
SignAndBroadcastTransactionInput,
SignTxInput,
ValidAddressResult,
} from '../../types'
import { ChainAdapterDisplayName, CONTRACT_INTERACTION } from '../../types'
import { ChainAdapterDisplayName, CONTRACT_INTERACTION, ValidAddressResultType } from '../../types'
import { toAddressNList } from '../../utils'
import { bnOrZero } from '../../utils/bignumber'
import { assertAddressNotSanctioned } from '../../utils/validateAddress'
Expand Down Expand Up @@ -110,6 +112,26 @@ export class ChainAdapter extends CosmosSdkBaseAdapter<KnownChainIds.ThorchainMa
}
}

// eslint-disable-next-line require-await
async validateAddress(address: string): Promise<ValidAddressResult> {
const THORCHAIN_PREFIX = 'thor'

try {
const decoded = bech32.decode(address)
if (decoded.prefix !== THORCHAIN_PREFIX) {
return { valid: false, result: ValidAddressResultType.Invalid }
}

const wordsLength = decoded.words.length
if (wordsLength !== 32) {
return { valid: false, result: ValidAddressResultType.Invalid }
}
return { valid: true, result: ValidAddressResultType.Valid }
} catch (e) {
return { valid: false, result: ValidAddressResultType.Invalid }
}
}

async signTransaction(signTxInput: SignTxInput<ThorchainSignTx>): Promise<string> {
try {
const { txToSign, wallet } = signTxInput
Expand Down
16 changes: 16 additions & 0 deletions scripts/generateAssetData/generateAssetData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dotenv/config'
import {
avalancheAssetId,
ethAssetId,
foxOnArbitrumOneAssetId,
fromAssetId,
gnosisAssetId,
polygonAssetId,
Expand Down Expand Up @@ -166,6 +167,21 @@ const generateAssetData = async () => {
generatedAssetData,
)

// Temporary workaround to circumvent the fact that no lists have that asset currently
const foxOnArbitrumOne = {
assetId: 'eip155:42161/erc20:0xf929de51d91c77e42f5090069e0ad7a09e513c73',
chainId: 'eip155:42161',
name: 'FOX on Arbitrum One',
precision: 18,
color: '#3761F9',
icon: 'https://assets.coincap.io/assets/icons/256/fox.png',
symbol: 'FOX',
explorer: 'https://arbiscan.io',
explorerAddressLink: 'https://arbiscan.io/address/',
explorerTxLink: 'https://arbiscan.io/tx/',
}
assetsWithOverridesApplied[foxOnArbitrumOneAssetId] = foxOnArbitrumOne

await fs.promises.writeFile(
path.join(__dirname, '../../src/lib/asset-service/service/generatedAssetData.json'),
// beautify the file for github diff.
Expand Down
26 changes: 15 additions & 11 deletions src/AppProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Provider as ReduxProvider } from 'react-redux'
import { HashRouter } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import { ScrollToTop } from 'Routes/ScrollToTop'
import { WagmiConfig } from 'wagmi'
import { ChatwootWidget } from 'components/ChatWoot'
import { AppProvider } from 'context/AppProvider/AppContext'
import { BrowserRouterProvider } from 'context/BrowserRouterProvider/BrowserRouterProvider'
Expand All @@ -27,6 +28,7 @@ import { KeepKeyProvider } from 'context/WalletProvider/KeepKeyProvider'
import { WalletProvider } from 'context/WalletProvider/WalletProvider'
import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton'
import { MixPanelEvent } from 'lib/mixpanel/types'
import { wagmiConfig } from 'lib/wagmi-config'
import { ErrorPage } from 'pages/ErrorPage/ErrorPage'
import { SplashScreen } from 'pages/SplashScreen/SplashScreen'
import { persistor, store } from 'state/store'
Expand Down Expand Up @@ -71,17 +73,19 @@ export function AppProviders({ children }: ProvidersProps) {
<WalletConnectV2Provider>
<KeepKeyProvider>
<ErrorBoundary FallbackComponent={ErrorPage} onError={handleError}>
<QueryClientProvider>
<ModalProvider>
<TransactionsProvider>
<AppProvider>
<FoxEthProvider>
<DefiManagerProvider>{children}</DefiManagerProvider>
</FoxEthProvider>
</AppProvider>
</TransactionsProvider>
</ModalProvider>
</QueryClientProvider>
<WagmiConfig config={wagmiConfig}>
<QueryClientProvider>
<ModalProvider>
<TransactionsProvider>
<AppProvider>
<FoxEthProvider>
<DefiManagerProvider>{children}</DefiManagerProvider>
</FoxEthProvider>
</AppProvider>
</TransactionsProvider>
</ModalProvider>
</QueryClientProvider>
</WagmiConfig>
</ErrorBoundary>
</KeepKeyProvider>
</WalletConnectV2Provider>
Expand Down
4 changes: 3 additions & 1 deletion src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"search": "Search",
"searchAsset": "Search for asset",
"enterAmount": "Enter Amount",
"enterAddress": "Enter Address",
"pageNotFound": "The page you're looking for cannot be found",
"buySell": "Buy/Sell",
"all": "All",
Expand All @@ -116,6 +117,7 @@
"feesPlusSlippage": "Fees + Slippage",
"best": "Best",
"gasFee": "Gas Fee",
"approvalFee" : "Approval Fee",
"consolidationFee": "Consolidation Fee",
"fees": "Fees",
"alternative": "Alternative",
Expand Down Expand Up @@ -2441,7 +2443,7 @@
"rewardAddressHelper": "This is where rewards will be sent",
"useWalletAddress": "Use wallet address",
"useCustomAddress": "Use custom address",
"stakeWarning": "When you stake your FOX tokens, please be aware that there is a %{cooldownPeriod} lock-up period. This means that if you decide to unstake your tokens, you will need to wait 28 days before you can claim your funds.",
"stakeWarning": "When you stake your FOX tokens, please be aware that there is a %{cooldownPeriod} lock-up period. This means that if you decide to unstake your tokens, you will need to wait %{cooldownPeriod} before you can claim your funds.",
"unstakeWarning": "Before you can claim your FOX, there is a %{cooldownPeriod} cool down period. After that time has elapsed you will be able to claim your unstaked amount.",
"tooltips": {
"stakeAmount": "This is the amount of FOX you will stake",
Expand Down
128 changes: 110 additions & 18 deletions src/components/MultiHopTrade/components/TradeAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import type { AccountId, AssetId } from '@shapeshiftoss/caip'
import noop from 'lodash/noop'
import type { ElementType, FocusEvent, PropsWithChildren } from 'react'
import React, { memo, useCallback, useMemo, useRef, useState } from 'react'
import type { FieldError } from 'react-hook-form'
import type { ControllerRenderProps, RegisterOptions } from 'react-hook-form'
import { Controller, type FieldError, useForm, useFormContext } from 'react-hook-form'
import type { NumberFormatValues } from 'react-number-format'
import NumberFormat from 'react-number-format'
import { useTranslate } from 'react-polyglot'
Expand All @@ -26,16 +27,30 @@ import { Balance } from 'components/DeFi/components/Balance'
import { PercentOptionsButtonGroup } from 'components/DeFi/components/PercentOptionsButtonGroup'
import { useLocaleFormatter } from 'hooks/useLocaleFormatter/useLocaleFormatter'
import { bnOrZero } from 'lib/bignumber/bignumber'
import { selectMarketDataByAssetIdUserCurrency } from 'state/slices/selectors'
import { useAppSelector } from 'state/store'
import { colors } from 'theme/colors'

import { usePriceImpactColor } from '../hooks/usePriceImpactColor'

export type TradeAmountInputFormValues = {
amountFieldInput: string
amountCryptoPrecision: string
amountUserCurrency: string
}

type RenderController = ({
field,
}: {
field: ControllerRenderProps<TradeAmountInputFormValues, 'amountFieldInput'>
}) => React.ReactElement

const cryptoInputStyle = { caretColor: colors.blue[200] }
const buttonProps = { variant: 'unstyled', display: 'flex', height: 'auto', lineHeight: '1' }
const boxProps = { px: 0, m: 0 }
const numberFormatDisabled = { opacity: 1, cursor: 'not-allowed' }

const CryptoInput = (props: InputProps) => {
const AmountInput = (props: InputProps) => {
const translate = useTranslate()
return (
<Input
Expand All @@ -50,12 +65,17 @@ const CryptoInput = (props: InputProps) => {
placeholder={translate('common.enterAmount')}
style={cryptoInputStyle}
autoComplete='off'
errorBorderColor='red.500'
{...props}
/>
)
}

export type TradeAmountInputProps = {
amountFieldInputRules?: Omit<
RegisterOptions,
'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'
>
autoSelectHighestBalance?: boolean
assetId?: AssetId
accountId?: AccountId
Expand Down Expand Up @@ -91,9 +111,17 @@ export type TradeAmountInputProps = {
} & PropsWithChildren

const defaultPercentOptions = [0.25, 0.5, 0.75, 1]
const defaultFormValues = {
amountFieldInput: '',
amountCryptoPrecision: '',
amountUserCurrency: '',
}

// TODO: While this is called "TradeAmountInput", its parent TradeAssetInput is consumed by everything under the sun but swapper
// Scrutinize this and rename all Trade references here, or at the very least in the parent to something more generic for sanity
export const TradeAmountInput: React.FC<TradeAmountInputProps> = memo(
({
amountFieldInputRules,
assetId,
accountId,
assetSymbol,
Expand Down Expand Up @@ -137,6 +165,20 @@ export const TradeAmountInput: React.FC<TradeAmountInputProps> = memo(
const focusBg = useColorModeValue('gray.50', 'gray.900')
const focusBorder = useColorModeValue('blue.500', 'blue.400')

const assetMarketDataUserCurrency = useAppSelector(state =>
selectMarketDataByAssetIdUserCurrency(state, assetId ?? ''),
)

// Local controller in case consumers don't have a form context, which is the case for all current consumers currently except RFOX
const _methods = useForm<TradeAmountInputFormValues>({
defaultValues: defaultFormValues,
mode: 'onChange',
shouldUnregister: true,
})
const methods = useFormContext<TradeAmountInputFormValues>()
const control = methods?.control ?? _methods.control
const setValue = methods?.setValue ?? _methods.setValue

// Lower the decimal places when the integer is greater than 8 significant digits for better UI
const cryptoAmountIntegerCount = bnOrZero(bnOrZero(cryptoAmount).toFixed(0)).precision(true)
const formattedCryptoAmount = useMemo(
Expand Down Expand Up @@ -172,12 +214,72 @@ export const TradeAmountInput: React.FC<TradeAmountInputProps> = memo(

const oppositeCurrency = useMemo(() => {
return isFiat ? (
<Amount.Crypto value={cryptoAmount ?? ''} symbol={assetSymbol} />
<Amount.Crypto value={cryptoAmount ?? ''} symbol={assetSymbol} prefix='≈' />
) : (
<Amount.Fiat value={fiatAmount ?? ''} prefix='≈' />
)
}, [assetSymbol, cryptoAmount, fiatAmount, isFiat])

const renderController: RenderController = useCallback(
({ field: { onChange } }) => {
return (
<NumberFormat
customInput={AmountInput}
isNumericString={true}
disabled={isReadOnly}
_disabled={numberFormatDisabled}
suffix={isFiat ? localeParts.postfix : ''}
prefix={isFiat ? localeParts.prefix : ''}
decimalSeparator={localeParts.decimal}
inputMode='decimal'
thousandSeparator={localeParts.group}
value={isFiat ? bnOrZero(fiatAmount).toFixed(2) : formattedCryptoAmount}
// this is already within a useCallback, we don't need to memo this
// eslint-disable-next-line react-memo/require-usememo
onValueChange={(values: NumberFormatValues) => {
// Controller onChange
onChange(values.value)
handleValueChange(values)

const value = values.value
if (isFiat) {
setValue('amountUserCurrency', value)
const _cryptoAmount = bnOrZero(value)
.div(assetMarketDataUserCurrency.price)
.toFixed()
setValue('amountCryptoPrecision', _cryptoAmount)
} else {
setValue('amountCryptoPrecision', value)
setValue(
'amountUserCurrency',
bnOrZero(value).times(assetMarketDataUserCurrency.price).toFixed(),
)
}
}}
onChange={handleOnChange}
onBlur={handleOnBlur}
onFocus={handleOnFocus}
/>
)
},
[
assetMarketDataUserCurrency.price,
fiatAmount,
formattedCryptoAmount,
handleOnBlur,
handleOnChange,
handleOnFocus,
handleValueChange,
isFiat,
isReadOnly,
localeParts.decimal,
localeParts.group,
localeParts.postfix,
localeParts.prefix,
setValue,
],
)

const accountDropdownLabel = useMemo(
() => (
<Balance
Expand Down Expand Up @@ -233,21 +335,11 @@ export const TradeAmountInput: React.FC<TradeAmountInputProps> = memo(
<Stack direction='row' alignItems='center' px={6} display={hideAmounts ? 'none' : 'flex'}>
<Flex gap={2} flex={1} alignItems='flex-end' pb={layout === 'inline' ? 4 : 0}>
<Skeleton isLoaded={!showInputSkeleton} width='full'>
<NumberFormat
customInput={CryptoInput}
isNumericString={true}
disabled={isReadOnly}
_disabled={numberFormatDisabled}
suffix={isFiat ? localeParts.postfix : ''}
prefix={isFiat ? localeParts.prefix : ''}
decimalSeparator={localeParts.decimal}
inputMode='decimal'
thousandSeparator={localeParts.group}
value={isFiat ? bnOrZero(fiatAmount).toFixed(2) : formattedCryptoAmount}
onValueChange={handleValueChange}
onChange={handleOnChange}
onBlur={handleOnBlur}
onFocus={handleOnFocus}
<Controller
name={'amountFieldInput'}
render={renderController}
control={control}
rules={amountFieldInputRules}
/>
</Skeleton>
{RightComponent && <RightComponent assetId={assetId ?? ''} />}
Expand Down
Loading

0 comments on commit e396ffb

Please sign in to comment.