Initial commit
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
|
||||
import { useAccounts, useStringGetter } from '@/hooks';
|
||||
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { ButtonAction } from '@/constants/buttons';
|
||||
|
||||
import { Button } from '@/components/Button';
|
||||
import { Link } from '@/components/Link';
|
||||
|
||||
type ElementProps = {
|
||||
onContinue?: () => void;
|
||||
};
|
||||
|
||||
export const AcknowledgeTerms = ({ onContinue }: ElementProps) => {
|
||||
const stringGetter = useStringGetter();
|
||||
|
||||
const { saveHasAcknowledgedTerms } = useAccounts();
|
||||
|
||||
const onAcknowledgement = () => {
|
||||
saveHasAcknowledgedTerms(true);
|
||||
onContinue?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>
|
||||
{stringGetter({
|
||||
key: STRING_KEYS.LEGAL_UPDATES_DESCRIPTION,
|
||||
params: {
|
||||
TOU: (
|
||||
<Styled.Link href="https://dydx.exchange/v4-terms">
|
||||
{stringGetter({ key: STRING_KEYS.TERMS_OF_USE })}
|
||||
</Styled.Link>
|
||||
),
|
||||
PRIVACY_POLICY: (
|
||||
<Styled.Link href="https://dydx.exchange/privacy">
|
||||
{stringGetter({ key: STRING_KEYS.PRIVACY_POLICY })}
|
||||
</Styled.Link>
|
||||
),
|
||||
},
|
||||
})}
|
||||
</span>
|
||||
<Button onClick={onAcknowledgement} action={ButtonAction.Primary}>
|
||||
{stringGetter({ key: STRING_KEYS.I_AGREE })}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Link = styled(Link)`
|
||||
display: inline-block;
|
||||
color: var(--color-accent);
|
||||
|
||||
&:visited {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useState } from 'react';
|
||||
import styled, { AnyStyledComponent } from 'styled-components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { AlertType } from '@/constants/alerts';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
|
||||
import { WalletType, wallets } from '@/constants/wallets';
|
||||
import { ButtonAction, ButtonSize } from '@/constants/buttons';
|
||||
|
||||
import { AlertMessage } from '@/components/AlertMessage';
|
||||
import { Button } from '@/components/Button';
|
||||
import { Icon } from '@/components/Icon';
|
||||
import { Link } from '@/components/Link';
|
||||
|
||||
import { useAccounts, useStringGetter } from '@/hooks';
|
||||
import { useDisplayedWallets } from '@/hooks/useDisplayedWallets';
|
||||
|
||||
import { breakpoints } from '@/styles';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
const aboutWalletsLink = `https://www.dydxacademy.info/educational-video-series/onboarding-to-defi-with-dydx`;
|
||||
|
||||
export const ChooseWallet = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
|
||||
const displayedWallets = useDisplayedWallets();
|
||||
|
||||
const { selectWalletType, selectedWalletType, selectedWalletError } = useAccounts();
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedWalletType && selectedWalletError && (
|
||||
<Styled.AlertMessage type={AlertType.Error}>
|
||||
{
|
||||
<h4>
|
||||
Couldn't connect to {stringGetter({ key: wallets[selectedWalletType].stringKey })}.
|
||||
</h4>
|
||||
}
|
||||
{selectedWalletError}
|
||||
</Styled.AlertMessage>
|
||||
)}
|
||||
|
||||
<Styled.Wallets>
|
||||
{displayedWallets.map((walletType) => (
|
||||
<Styled.WalletButton
|
||||
action={ButtonAction.Base}
|
||||
key={walletType}
|
||||
onClick={() => selectWalletType(walletType)}
|
||||
slotLeft={<Styled.Icon iconComponent={wallets[walletType].icon} />}
|
||||
size={ButtonSize.Small}
|
||||
>
|
||||
<div>{stringGetter({ key: wallets[walletType].stringKey })}</div>
|
||||
</Styled.WalletButton>
|
||||
))}
|
||||
</Styled.Wallets>
|
||||
|
||||
<Styled.Footer>
|
||||
<Link href={aboutWalletsLink} withIcon>
|
||||
{stringGetter({ key: STRING_KEYS.ABOUT_WALLETS })}
|
||||
</Link>
|
||||
</Styled.Footer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.AlertMessage = styled(AlertMessage)`
|
||||
h4 {
|
||||
font: var(--font-small-medium);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Wallets = styled.div`
|
||||
gap: 0.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
||||
|
||||
> :last-child:nth-child(odd) {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
// Flex layout
|
||||
/* display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
:after {
|
||||
content: '';
|
||||
flex: 2;
|
||||
} */
|
||||
`;
|
||||
|
||||
Styled.WalletButton = styled(Button)`
|
||||
justify-content: start;
|
||||
gap: 0.5rem;
|
||||
|
||||
@media ${breakpoints.mobile} {
|
||||
div {
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Icon = styled(Icon)`
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
`;
|
||||
|
||||
Styled.Footer = styled.footer`
|
||||
${layoutMixins.spacedRow}
|
||||
justify-content: center;
|
||||
margin-top: auto;
|
||||
|
||||
a {
|
||||
color: var(--color-text-0);
|
||||
font: var(--font-base-book);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,334 @@
|
||||
import { useState } from 'react';
|
||||
import { useSignTypedData } from 'wagmi';
|
||||
import styled, { type AnyStyledComponent, css } from 'styled-components';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AES } from 'crypto-js';
|
||||
|
||||
import { EvmDerivedAccountStatus } from '@/constants/account';
|
||||
import { AlertType } from '@/constants/alerts';
|
||||
import { AnalyticsEvent } from '@/constants/analytics';
|
||||
import { ButtonAction } from '@/constants/buttons';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks';
|
||||
import { DydxAddress, SIGN_TYPED_DATA } from '@/constants/wallets';
|
||||
|
||||
import { useAccounts, useBreakpoints, useDydxClient, useStringGetter } from '@/hooks';
|
||||
import { useMatchingEvmNetwork } from '@/hooks/useMatchingEvmNetwork';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { AlertMessage } from '@/components/AlertMessage';
|
||||
import { Button } from '@/components/Button';
|
||||
import { GreenCheckCircle } from '@/components/GreenCheckCircle';
|
||||
import { LoadingSpinner } from '@/components/Loading/LoadingSpinner';
|
||||
import { Switch } from '@/components/Switch';
|
||||
import { WithReceipt } from '@/components/WithReceipt';
|
||||
import { WithTooltip } from '@/components/WithTooltip';
|
||||
|
||||
import { getSelectedNetwork } from '@/state/appSelectors';
|
||||
|
||||
import { track } from '@/lib/analytics';
|
||||
import { isTruthy } from '@/lib/isTruthy';
|
||||
import { log } from '@/lib/telemetry';
|
||||
import { parseWalletError } from '@/lib/wallet';
|
||||
|
||||
type ElementProps = {
|
||||
status: EvmDerivedAccountStatus;
|
||||
setStatus: (status: EvmDerivedAccountStatus) => void;
|
||||
onKeysDerived?: () => void;
|
||||
};
|
||||
|
||||
export const GenerateKeys = ({
|
||||
status: status,
|
||||
setStatus,
|
||||
onKeysDerived = () => {},
|
||||
}: ElementProps) => {
|
||||
const stringGetter = useStringGetter();
|
||||
const { isMobile } = useBreakpoints();
|
||||
|
||||
const [shouldRememberMe, setShouldRememberMe] = useState(false);
|
||||
|
||||
const { setWalletFromEvmSignature, saveEvmSignature } = useAccounts();
|
||||
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
// 1. Switch network
|
||||
const selectedNetwork = useSelector(getSelectedNetwork);
|
||||
|
||||
const chainId = Number(CLIENT_NETWORK_CONFIGS[selectedNetwork].ethereumChainId);
|
||||
|
||||
const { isMatchingNetwork, matchNetwork, isSwitchingNetwork } = useMatchingEvmNetwork({
|
||||
chainId,
|
||||
});
|
||||
|
||||
const switchNetwork = async () => {
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
await matchNetwork?.();
|
||||
} catch (error) {
|
||||
const { message, walletErrorType } = parseWalletError({ error, stringGetter });
|
||||
|
||||
if (message) {
|
||||
log('GenerateKeys/switchNetwork', error, { walletErrorType });
|
||||
setError(message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Derive keys from EVM account
|
||||
const { getWalletFromEvmSignature } = useDydxClient();
|
||||
const { getSubaccounts } = useAccounts();
|
||||
|
||||
const isDeriving = ![
|
||||
EvmDerivedAccountStatus.NotDerived,
|
||||
EvmDerivedAccountStatus.Derived,
|
||||
].includes(status);
|
||||
|
||||
const { signTypedDataAsync } = useSignTypedData({
|
||||
...SIGN_TYPED_DATA,
|
||||
domain: {
|
||||
...SIGN_TYPED_DATA.domain,
|
||||
chainId,
|
||||
},
|
||||
});
|
||||
|
||||
const staticEncryptionKey = import.meta.env.VITE_PK_ENCRYPTION_KEY;
|
||||
|
||||
const deriveKeys = async () => {
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
// 1. First signature
|
||||
setStatus(EvmDerivedAccountStatus.Deriving);
|
||||
|
||||
const signature = await signTypedDataAsync();
|
||||
const { wallet: dydxWallet } = await getWalletFromEvmSignature({ signature });
|
||||
|
||||
// 2. Ensure signature is deterministic
|
||||
// Check if subaccounts exist
|
||||
const dydxAddress = dydxWallet.address as DydxAddress;
|
||||
let hasPreviousTransactions = false;
|
||||
|
||||
try {
|
||||
const subaccounts = await getSubaccounts({ dydxAddress });
|
||||
hasPreviousTransactions = subaccounts.length > 0;
|
||||
|
||||
track(AnalyticsEvent.OnboardingAccountDerived, { hasPreviousTransactions });
|
||||
|
||||
if (!hasPreviousTransactions) {
|
||||
setStatus(EvmDerivedAccountStatus.EnsuringDeterminism);
|
||||
|
||||
// Second signature
|
||||
const additionalSignature = await signTypedDataAsync();
|
||||
|
||||
if (signature !== additionalSignature) {
|
||||
throw new Error(
|
||||
'Your wallet does not support deterministic signing. Please switch to a different wallet provider.'
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const { message } = parseWalletError({ error, stringGetter });
|
||||
|
||||
if (message) {
|
||||
track(AnalyticsEvent.OnboardingWalletIsNonDeterministic);
|
||||
setError(message);
|
||||
}
|
||||
}
|
||||
|
||||
await setWalletFromEvmSignature(signature);
|
||||
|
||||
// 3: Remember me (encrypt and store signature)
|
||||
if (shouldRememberMe && staticEncryptionKey) {
|
||||
const encryptedSignature = AES.encrypt(signature, staticEncryptionKey).toString();
|
||||
|
||||
saveEvmSignature(encryptedSignature);
|
||||
}
|
||||
|
||||
// 4. Done
|
||||
setStatus(EvmDerivedAccountStatus.Derived);
|
||||
} catch (error) {
|
||||
setStatus(EvmDerivedAccountStatus.NotDerived);
|
||||
const { message, walletErrorType } = parseWalletError({ error, stringGetter });
|
||||
|
||||
if (message) {
|
||||
setError(message);
|
||||
log('GenerateKeys/deriveKeys', error, { walletErrorType });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Styled.StatusCardsContainer>
|
||||
{[
|
||||
{
|
||||
status: EvmDerivedAccountStatus.Deriving,
|
||||
title: stringGetter({ key: STRING_KEYS.GENERATE_COSMOS_WALLET }),
|
||||
description: stringGetter({ key: STRING_KEYS.GENERATE_COSMOS_WALLET }),
|
||||
},
|
||||
status === EvmDerivedAccountStatus.EnsuringDeterminism && {
|
||||
status: EvmDerivedAccountStatus.EnsuringDeterminism,
|
||||
title: stringGetter({ key: STRING_KEYS.VERIFY_WALLET_COMPATIBILITY }),
|
||||
description: stringGetter({ key: STRING_KEYS.ENSURES_WALLET_SUPPORT }),
|
||||
},
|
||||
]
|
||||
.filter(isTruthy)
|
||||
.map((step) => (
|
||||
<Styled.StatusCard key={step.status} active={status === step.status}>
|
||||
{status < step.status ? (
|
||||
<LoadingSpinner disabled />
|
||||
) : status === step.status ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<Styled.GreenCheckCircle />
|
||||
)}
|
||||
<div>
|
||||
<h3>{step.title}</h3>
|
||||
<p>{step.description}</p>
|
||||
</div>
|
||||
</Styled.StatusCard>
|
||||
))}
|
||||
</Styled.StatusCardsContainer>
|
||||
|
||||
<Styled.Footer>
|
||||
<Styled.RememberMe htmlFor="remember-me">
|
||||
<WithTooltip withIcon tooltip="remember-me">
|
||||
{stringGetter({ key: STRING_KEYS.REMEMBER_ME })}
|
||||
</WithTooltip>
|
||||
|
||||
<Switch
|
||||
name="remember-me"
|
||||
disabled={!staticEncryptionKey || isDeriving}
|
||||
checked={shouldRememberMe}
|
||||
onCheckedChange={setShouldRememberMe}
|
||||
/>
|
||||
</Styled.RememberMe>
|
||||
{error && <AlertMessage type={AlertType.Error}>{error}</AlertMessage>}
|
||||
<Styled.WithReceipt
|
||||
slotReceipt={
|
||||
<Styled.ReceiptArea>
|
||||
<span>
|
||||
{stringGetter({
|
||||
key: STRING_KEYS.FREE_SIGNING,
|
||||
params: {
|
||||
FREE: (
|
||||
<Styled.Green>
|
||||
{stringGetter({ key: STRING_KEYS.FREE_TRADING_TITLE_ASTERISK_FREE })}
|
||||
</Styled.Green>
|
||||
),
|
||||
},
|
||||
})}
|
||||
</span>
|
||||
</Styled.ReceiptArea>
|
||||
}
|
||||
>
|
||||
{!isMatchingNetwork ? (
|
||||
<Button
|
||||
action={ButtonAction.Primary}
|
||||
onClick={() => switchNetwork().then(deriveKeys).then(onKeysDerived)}
|
||||
state={{ isLoading: isSwitchingNetwork }}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.SWITCH_NETWORK })}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
action={ButtonAction.Primary}
|
||||
onClick={() => deriveKeys().then(onKeysDerived)}
|
||||
state={{
|
||||
isLoading: isDeriving,
|
||||
isDisabled: status !== EvmDerivedAccountStatus.NotDerived,
|
||||
}}
|
||||
>
|
||||
{!error
|
||||
? stringGetter({
|
||||
key: STRING_KEYS.SEND_REQUEST,
|
||||
})
|
||||
: stringGetter({
|
||||
key: STRING_KEYS.TRY_AGAIN,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</Styled.WithReceipt>
|
||||
<Styled.Disclaimer>
|
||||
{stringGetter({ key: STRING_KEYS.CHECK_WALLET_FOR_REQUEST })}
|
||||
</Styled.Disclaimer>
|
||||
</Styled.Footer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.StatusCardsContainer = styled.div`
|
||||
display: grid;
|
||||
margin-top: 1rem;
|
||||
gap: 1rem;
|
||||
`;
|
||||
|
||||
Styled.StatusCard = styled.div<{ active?: boolean }>`
|
||||
${layoutMixins.row}
|
||||
gap: 1rem;
|
||||
background-color: var(--color-layer-4);
|
||||
padding: 1rem;
|
||||
border-radius: 0.625rem;
|
||||
|
||||
${({ active }) =>
|
||||
active &&
|
||||
css`
|
||||
background-color: var(--color-layer-6);
|
||||
`}
|
||||
|
||||
> div {
|
||||
${layoutMixins.column}
|
||||
gap: 0.25rem;
|
||||
|
||||
h3 {
|
||||
color: var(--color-text-2);
|
||||
font: var(--font-base-book);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--color-text-1);
|
||||
font: var(--font-small-regular);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Footer = styled.footer`
|
||||
${layoutMixins.stickyFooter}
|
||||
margin-top: auto;
|
||||
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
`;
|
||||
|
||||
Styled.RememberMe = styled.label`
|
||||
${layoutMixins.spacedRow}
|
||||
font: var(--font-base-book);
|
||||
`;
|
||||
|
||||
Styled.WithReceipt = styled(WithReceipt)`
|
||||
--withReceipt-backgroundColor: var(--color-layer-2);
|
||||
`;
|
||||
|
||||
Styled.ReceiptArea = styled.div`
|
||||
padding: 1rem;
|
||||
font: var(--font-small-book);
|
||||
color: var(--color-text-0);
|
||||
`;
|
||||
|
||||
Styled.Green = styled.span`
|
||||
color: var(--color-positive);
|
||||
`;
|
||||
|
||||
Styled.GreenCheckCircle = styled(GreenCheckCircle)`
|
||||
--icon-size: 2.375rem;
|
||||
`;
|
||||
|
||||
Styled.Disclaimer = styled.span`
|
||||
text-align: center;
|
||||
color: var(--color-text-0);
|
||||
font: var(--font-base-book);
|
||||
`;
|
||||
Reference in New Issue
Block a user