-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(tangle-dapp): Restake Delegation (#2414)
- Loading branch information
Showing
42 changed files
with
2,043 additions
and
537 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { useConnectWallet } from '@webb-tools/api-provider-environment/ConnectWallet'; | ||
import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; | ||
import Button from '@webb-tools/webb-ui-components/components/buttons/Button'; | ||
import { ConnectWalletMobileButton } from '@webb-tools/webb-ui-components/components/ConnectWalletMobileButton'; | ||
import { useCheckMobile } from '@webb-tools/webb-ui-components/hooks/useCheckMobile'; | ||
import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; | ||
import Link from 'next/link'; | ||
import { type ReactNode, useMemo } from 'react'; | ||
|
||
type Props = { | ||
targetTypedChainId?: number; | ||
children: (isLoading: boolean, loadingText?: string) => ReactNode; | ||
}; | ||
|
||
export default function ActionButtonBase({ | ||
targetTypedChainId, | ||
children, | ||
}: Props) { | ||
const { isMobile } = useCheckMobile(); | ||
const { loading, isConnecting, activeWallet } = useWebContext(); | ||
const { toggleModal } = useConnectWallet(); | ||
|
||
const { isLoading, loadingText } = useMemo(() => { | ||
if (loading) | ||
return { | ||
isLoading: true, | ||
loadingText: 'Loading...', | ||
}; | ||
|
||
if (isConnecting) | ||
return { | ||
isLoading: true, | ||
loadingText: 'Connecting...', | ||
}; | ||
|
||
return { | ||
isLoading: false, | ||
}; | ||
}, [isConnecting, loading]); | ||
|
||
if (isMobile) { | ||
return ( | ||
<ConnectWalletMobileButton title="Try Hubble on Desktop" isFullWidth> | ||
<Typography variant="body1"> | ||
A complete mobile experience for Hubble Bridge is in the works. For | ||
now, enjoy all features on a desktop device. | ||
</Typography> | ||
<Typography variant="body1"> | ||
Visit the link on desktop below to start transacting privately! | ||
</Typography> | ||
<Button as={Link} href="deposit" variant="link"> | ||
{window.location.href} | ||
</Button> | ||
</ConnectWalletMobileButton> | ||
); | ||
} | ||
|
||
// If the user is not connected to a wallet, show the connect wallet button | ||
if (activeWallet === undefined) { | ||
return ( | ||
<Button | ||
type="button" | ||
isFullWidth | ||
isLoading={isLoading} | ||
loadingText={loadingText} | ||
onClick={() => toggleModal(true, targetTypedChainId)} | ||
> | ||
Connect Wallet | ||
</Button> | ||
); | ||
} | ||
|
||
return children(isLoading, loadingText); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { isEthereumAddress } from '@polkadot/util-crypto'; | ||
import { getFlexBasic } from '@webb-tools/icons/utils'; | ||
import { Avatar } from '@webb-tools/webb-ui-components/components/Avatar'; | ||
import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; | ||
import { shortenHex } from '@webb-tools/webb-ui-components/utils/shortenHex'; | ||
import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenString'; | ||
import isEqual from 'lodash/isEqual'; | ||
import { type ComponentProps, memo } from 'react'; | ||
import { twMerge } from 'tailwind-merge'; | ||
import { isHex } from 'viem'; | ||
|
||
type Props = ComponentProps<'div'> & { | ||
accountAddress: string; | ||
overrideAvatarProps?: Partial<ComponentProps<typeof Avatar>>; | ||
overrideTypographyProps?: Partial<ComponentProps<typeof Typography>>; | ||
}; | ||
|
||
const AvatarWithText = ({ | ||
accountAddress, | ||
overrideAvatarProps, | ||
overrideTypographyProps, | ||
className, | ||
...props | ||
}: Props) => { | ||
return ( | ||
<div | ||
{...props} | ||
className={twMerge( | ||
'flex items-center max-w-xs space-x-2 grow', | ||
className, | ||
)} | ||
> | ||
<Avatar | ||
// TODO: Determine the theme instead of hardcoding it | ||
theme={isEthereumAddress(accountAddress) ? 'ethereum' : 'substrate'} | ||
value={accountAddress} | ||
{...overrideAvatarProps} | ||
className={twMerge( | ||
`${getFlexBasic()} shrink-0`, | ||
overrideAvatarProps?.className, | ||
)} | ||
/> | ||
|
||
<Typography | ||
variant="body2" | ||
{...overrideTypographyProps} | ||
className={twMerge('truncate', overrideTypographyProps?.className)} | ||
> | ||
{isHex(accountAddress) | ||
? shortenHex(accountAddress) | ||
: shortenString(accountAddress)} | ||
</Typography> | ||
</div> | ||
); | ||
}; | ||
|
||
export default memo(AvatarWithText, (prevProps, nextProps) => | ||
isEqual(prevProps, nextProps), | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
'use client'; | ||
|
||
import { useWebContext } from '@webb-tools/api-provider-environment/webb-context'; | ||
import { calculateTypedChainId } from '@webb-tools/sdk-core/typed-chain-id'; | ||
import ChainListCard from '@webb-tools/webb-ui-components/components/ListCard/ChainListCard'; | ||
import { type ComponentProps, useMemo } from 'react'; | ||
import { twMerge } from 'tailwind-merge'; | ||
|
||
import { SUPPORTED_RESTAKE_DEPOSIT_TYPED_CHAIN_IDS } from '../../constants/restake'; | ||
|
||
type Props = Partial<ComponentProps<typeof ChainListCard>> & { | ||
selectedTypedChainId?: number | null; | ||
}; | ||
|
||
const ChainList = ({ | ||
className, | ||
onClose, | ||
selectedTypedChainId, | ||
...props | ||
}: Props) => { | ||
const { activeChain, loading, apiConfig } = useWebContext(); | ||
|
||
const selectedChain = useMemo( | ||
() => | ||
typeof selectedTypedChainId === 'number' | ||
? apiConfig.chains[selectedTypedChainId] | ||
: null, | ||
[apiConfig.chains, selectedTypedChainId], | ||
); | ||
|
||
const chains = useMemo( | ||
() => | ||
SUPPORTED_RESTAKE_DEPOSIT_TYPED_CHAIN_IDS.map( | ||
(typedChainId) => | ||
[typedChainId, apiConfig.chains[typedChainId]] as const, | ||
) | ||
.filter(([, chain]) => Boolean(chain)) | ||
.map(([typedChainId, chain]) => ({ | ||
typedChainId, | ||
name: chain.name, | ||
tag: chain.tag, | ||
needSwitchWallet: | ||
activeChain?.id !== chain.id && | ||
activeChain?.chainType !== chain.chainType, | ||
})), | ||
[activeChain?.chainType, activeChain?.id, apiConfig.chains], | ||
); | ||
|
||
const defaultCategory = useMemo< | ||
ComponentProps<typeof ChainListCard>['defaultCategory'] | ||
>(() => { | ||
return selectedChain?.tag ?? activeChain?.tag ?? 'test'; | ||
}, [activeChain?.tag, selectedChain?.tag]); | ||
|
||
return ( | ||
<ChainListCard | ||
chainType="source" | ||
overrideTitleProps={{ | ||
variant: 'h4', | ||
}} | ||
chains={chains} | ||
activeTypedChainId={ | ||
activeChain | ||
? calculateTypedChainId(activeChain.chainType, activeChain.id) | ||
: undefined | ||
} | ||
defaultCategory={defaultCategory} | ||
isConnectingToChain={loading} | ||
overrideScrollAreaProps={{ | ||
className: 'h-[320px]', | ||
}} | ||
{...props} | ||
onClose={onClose} | ||
className={twMerge( | ||
'p-0 dark:bg-[var(--restake-card-bg-dark)]', | ||
className, | ||
)} | ||
/> | ||
); | ||
}; | ||
|
||
ChainList.displayName = 'ChainList'; | ||
|
||
export default ChainList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import isDefined from '@webb-tools/dapp-types/utils/isDefined'; | ||
import { InformationLine } from '@webb-tools/icons'; | ||
import type { PropsOf } from '@webb-tools/webb-ui-components/types'; | ||
import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; | ||
import { ComponentProps } from 'react'; | ||
import { twMerge } from 'tailwind-merge'; | ||
|
||
type Props = PropsOf<'p'> & { | ||
typographyProps?: Partial<ComponentProps<typeof Typography>>; | ||
}; | ||
|
||
export default function ErrorMessage({ | ||
children, | ||
className, | ||
typographyProps: { | ||
variant = 'body4', | ||
className: typoClassName, | ||
...typographyProps | ||
} = {}, | ||
...props | ||
}: Props) { | ||
return ( | ||
<p | ||
{...props} | ||
className={twMerge( | ||
'flex items-center gap-1 text-red-70 dark:text-red-50 h-4 mt-2', | ||
className, | ||
)} | ||
> | ||
{isDefined(children) ? ( | ||
<InformationLine size="md" className="!fill-current" /> | ||
) : null} | ||
|
||
<Typography | ||
component="span" | ||
fw="bold" | ||
{...typographyProps} | ||
variant={variant} | ||
className={twMerge('!text-inherit', typoClassName)} | ||
> | ||
{children} | ||
</Typography> | ||
</p> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { Transition, TransitionRootProps } from '@headlessui/react'; | ||
import type { PropsWithChildren } from 'react'; | ||
import { twMerge } from 'tailwind-merge'; | ||
|
||
type Props = Partial<TransitionRootProps<'div'>> & | ||
Pick<TransitionRootProps<'div'>, 'show'> & | ||
Omit<TransitionRootProps<'div'>, 'as'> & { | ||
className?: string; | ||
}; | ||
|
||
export default function SlideAnimation({ | ||
children, | ||
className, | ||
enter, | ||
enterFrom, | ||
enterTo, | ||
leave, | ||
leaveFrom, | ||
leaveTo, | ||
...restProps | ||
}: PropsWithChildren<Props>) { | ||
return ( | ||
<Transition | ||
{...restProps} | ||
as="div" | ||
className={twMerge('h-full w-full', className)} | ||
enter={twMerge('transition-[top,_opacity] duration-150', enter)} | ||
enterFrom={twMerge('opacity-0 top-[250px]', enterFrom)} | ||
enterTo={twMerge('opacity-100 top-0', enterTo)} | ||
leave={twMerge('transition-[top,_opacity] duration-150', leave)} | ||
leaveFrom={twMerge('opacity-100 top-0', leaveFrom)} | ||
leaveTo={twMerge('opacity-0 top-[250px]', leaveTo)} | ||
> | ||
{children} | ||
</Transition> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import isDefined from '@webb-tools/dapp-types/utils/isDefined'; | ||
import type { Noop } from '@webb-tools/dapp-types/utils/types'; | ||
import Button from '@webb-tools/webb-ui-components/components/buttons/Button'; | ||
import { useMemo } from 'react'; | ||
import type { FieldErrors, UseFormWatch } from 'react-hook-form'; | ||
|
||
import { SUPPORTED_RESTAKE_DEPOSIT_TYPED_CHAIN_IDS } from '../../../constants/restake'; | ||
import useActiveTypedChainId from '../../../hooks/useActiveTypedChainId'; | ||
import { DelegationFormFields } from '../../../types/restake'; | ||
import ActionButtonBase from '../ActionButtonBase'; | ||
|
||
type Props = { | ||
openChainModal: Noop; | ||
isValid: boolean; | ||
isSubmitting: boolean; | ||
errors: FieldErrors<DelegationFormFields>; | ||
watch: UseFormWatch<DelegationFormFields>; | ||
}; | ||
|
||
export default function ActionButton({ | ||
openChainModal, | ||
isValid, | ||
isSubmitting, | ||
errors, | ||
watch, | ||
}: Props) { | ||
const activeTypedChainId = useActiveTypedChainId(); | ||
const operatorAccountId = watch('operatorAccountId'); | ||
const assetId = watch('assetId'); | ||
const amount = watch('amount'); | ||
|
||
const displayError = useMemo( | ||
() => { | ||
return errors.operatorAccountId !== undefined || !operatorAccountId | ||
? 'Select an operator' | ||
: errors.assetId !== undefined || !assetId | ||
? 'Select an asset' | ||
: !amount | ||
? 'Enter an amount' | ||
: errors.amount !== undefined | ||
? 'Invalid amount' | ||
: undefined; | ||
}, | ||
// prettier-ignore | ||
[errors.operatorAccountId, errors.assetId, errors.amount, operatorAccountId, assetId, amount], | ||
); | ||
|
||
return ( | ||
<ActionButtonBase> | ||
{(isLoading, loadingText) => { | ||
const activeChainSupported = | ||
isDefined(activeTypedChainId) && | ||
SUPPORTED_RESTAKE_DEPOSIT_TYPED_CHAIN_IDS.includes( | ||
activeTypedChainId, | ||
); | ||
|
||
if (!activeChainSupported) { | ||
return ( | ||
<Button | ||
isFullWidth | ||
type="button" | ||
isLoading={isLoading} | ||
loadingText={loadingText} | ||
onClick={openChainModal} | ||
> | ||
Switch to supported chain | ||
</Button> | ||
); | ||
} | ||
|
||
return ( | ||
<Button | ||
isDisabled={!isValid || isDefined(displayError)} | ||
type="submit" | ||
isFullWidth | ||
isLoading={isSubmitting || isLoading} | ||
loadingText={isSubmitting ? 'Delegating...' : loadingText} | ||
> | ||
{displayError ?? 'Delegate'} | ||
</Button> | ||
); | ||
}} | ||
</ActionButtonBase> | ||
); | ||
} |
Oops, something went wrong.