Coinbase Deposit (#251)

* Coinbase Deposit

* bump packages

* fix lock

* add testflag

* Address feedback

* fix chain select
This commit is contained in:
Bill 2024-01-29 15:12:06 -08:00 committed by GitHub
parent 41cc531700
commit 91f5b89eb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 262 additions and 54 deletions

View File

@ -0,0 +1,8 @@
[
{
"name": "coinbase",
"label": "Coinbase",
"icon": "/exchanges/coinbase.png",
"depositType": "noble"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -11,6 +11,7 @@ type ElementProps = {
};
type StyleProps = {
className?: string;
hasLogo?: boolean;
size?: number;
};
@ -18,7 +19,7 @@ type StyleProps = {
const DARK_LOGO_MARK_URL = '/logos/logo-mark-dark.svg';
const LIGHT_LOGO_MARK_URL = '/logos/logo-mark-light.svg';
export const QrCode = ({ value, hasLogo, size = 300 }: ElementProps & StyleProps) => {
export const QrCode = ({ className, value, hasLogo, size = 300 }: ElementProps & StyleProps) => {
const ref = useRef<HTMLDivElement>(null);
const appTheme: AppTheme = useSelector(getAppTheme);
@ -74,7 +75,7 @@ export const QrCode = ({ value, hasLogo, size = 300 }: ElementProps & StyleProps
}
}, [appTheme, hasLogo]);
return <Styled.QrCode ref={ref} />;
return <Styled.QrCode className={className} ref={ref} />;
};
const Styled: Record<string, AnyStyledComponent> = {};

View File

@ -8,6 +8,7 @@ import { Button, type ButtonStateConfig, type ButtonProps } from '@/components/B
type ElementProps = {
timeoutInSeconds: number;
slotFinal?: ReactNode;
} & ButtonProps;
export type TimeoutButtonProps = ElementProps;
@ -15,6 +16,7 @@ export type TimeoutButtonProps = ElementProps;
export const TimeoutButton = ({
children,
timeoutInSeconds,
slotFinal,
...otherProps
}: TimeoutButtonProps) => {
const [timeoutDeadline] = useState(Date.now() + timeoutInSeconds * 1000);
@ -23,6 +25,8 @@ export const TimeoutButton = ({
const secondsLeft = Math.max(0, (timeoutDeadline - now) / 1000);
if (slotFinal && secondsLeft <= 0) return slotFinal;
return (
<Button
{...otherProps}

View File

@ -161,6 +161,7 @@ const useAccountsContext = () => {
// dYdX wallet / onboarding state
const [localDydxWallet, setLocalDydxWallet] = useState<LocalWallet>();
const [localNobleWallet, setLocalNobleWallet] = useState<LocalWallet>();
const [hdKey, setHdKey] = useState<PrivateInformation>();
const dydxAccounts = useMemo(() => localDydxWallet?.accounts, [localDydxWallet]);
@ -170,6 +171,11 @@ const useAccountsContext = () => {
[localDydxWallet]
);
const nobleAddress = useMemo(
() => localNobleWallet?.address,
[localNobleWallet]
);
const setWalletFromEvmSignature = async (signature: string) => {
const { wallet, mnemonic, privateKey, publicKey } = await getWalletFromEvmSignature({
signature,
@ -251,6 +257,7 @@ const useAccountsContext = () => {
if (hdKey?.mnemonic) {
const nobleWallet = await LocalWallet.fromMnemonic(hdKey.mnemonic, NOBLE_BECH32_PREFIX);
abacusStateManager.setNobleWallet(nobleWallet);
setLocalNobleWallet(nobleWallet);
}
};
setNobleWallet();
@ -349,6 +356,7 @@ const useAccountsContext = () => {
localDydxWallet,
dydxAccounts,
dydxAddress,
nobleAddress,
// Onboarding state
saveHasAcknowledgedTerms,

View File

@ -26,6 +26,10 @@ class TestFlags {
get addressOverride():string {
return this.queryParams.address;
}
get showCEXDepositOption() {
return !!this.queryParams.cexdeposit;
}
}
export const testFlags = new TestFlags();

View File

@ -17,7 +17,6 @@ import type { EvmAddress } from '@/constants/wallets';
import { useAccounts, useDebounce, useStringGetter, useSelectedNetwork } from '@/hooks';
import { useAccountBalance, CHAIN_DEFAULT_TOKEN_ADDRESS } from '@/hooks/useAccountBalance';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { useWalletConnection } from '@/hooks/useWalletConnection';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
@ -41,10 +40,11 @@ import { getNobleChainId, NATIVE_TOKEN_ADDRESS } from '@/lib/squid';
import { log } from '@/lib/telemetry';
import { parseWalletError } from '@/lib/wallet';
import { ChainSelectMenu } from './ChainSelectMenu';
import { SourceSelectMenu } from './SourceSelectMenu';
import { TokenSelectMenu } from './TokenSelectMenu';
import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt';
import { NobleDeposit } from '../NobleDeposit';
type DepositFormProps = {
onDeposit?: () => void;
@ -57,13 +57,14 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const [isLoading, setIsLoading] = useState(false);
const { selectedNetwork } = useSelectedNetwork();
const { evmAddress, signerWagmi, publicClientWagmi } = useAccounts();
const { evmAddress, signerWagmi, publicClientWagmi, nobleAddress } = useAccounts();
const { addTransferNotification } = useLocalNotifications();
const {
requestPayload,
token,
exchange,
chain: chainIdStr,
resources,
summary,
@ -129,14 +130,21 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
if (error) onError?.();
}, [error]);
const onSelectChain = useCallback((chain: string) => {
if (chain) {
const onSelectChain = useCallback((name: string, type: 'chain' | 'exchange') => {
if (name) {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
value: chain,
});
setFromAmount('');
if (type === 'chain') {
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
value: name,
});
} else {
abacusStateManager.setTransferValue({
field: TransferInputField.exchange,
value: name,
});
}
}
}, []);
@ -369,39 +377,49 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
return (
<Styled.Form onSubmit={onSubmit}>
<ChainSelectMenu selectedChain={chainIdStr || undefined} onSelectChain={onSelectChain} />
<TokenSelectMenu selectedToken={sourceToken || undefined} onSelectToken={onSelectToken} />
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<FormInput
type={InputType.Number}
onChange={onChangeAmount}
label={stringGetter({ key: STRING_KEYS.AMOUNT })}
value={fromAmount}
slotRight={
<Styled.FormInputButton size={ButtonSize.XSmall} onClick={onClickMax}>
{stringGetter({ key: STRING_KEYS.MAX })}
</Styled.FormInputButton>
}
/>
</Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
{requireUserActionInWallet && (
<AlertMessage type={AlertType.Warning}>
{stringGetter({ key: STRING_KEYS.CHECK_WALLET_FOR_REQUEST })}
</AlertMessage>
<SourceSelectMenu
selectedChain={chainIdStr || undefined}
selectedExchange={exchange || undefined}
onSelect={onSelectChain}
/>
{exchange && nobleAddress ? (
<NobleDeposit />
) : (
<>
<TokenSelectMenu selectedToken={sourceToken || undefined} onSelectToken={onSelectToken} />
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<FormInput
type={InputType.Number}
onChange={onChangeAmount}
label={stringGetter({ key: STRING_KEYS.AMOUNT })}
value={fromAmount}
slotRight={
<Styled.FormInputButton size={ButtonSize.XSmall} onClick={onClickMax}>
{stringGetter({ key: STRING_KEYS.MAX })}
</Styled.FormInputButton>
}
/>
</Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
{requireUserActionInWallet && (
<AlertMessage type={AlertType.Warning}>
{stringGetter({ key: STRING_KEYS.CHECK_WALLET_FOR_REQUEST })}
</AlertMessage>
)}
<Styled.Footer>
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
chainId={chainId || undefined}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken || undefined}
setRequireUserActionInWallet={setRequireUserActionInWallet}
setError={setError}
/>
</Styled.Footer>
</>
)}
<Styled.Footer>
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
chainId={chainId || undefined}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken || undefined}
setRequireUserActionInWallet={setRequireUserActionInWallet}
setError={setError}
/>
</Styled.Footer>
</Styled.Form>
);
};

View File

@ -3,6 +3,7 @@ import { shallowEqual, useSelector } from 'react-redux';
import { TransferType } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { SearchSelectMenu } from '@/components/SearchSelectMenu';
@ -12,45 +13,78 @@ import { popoverMixins } from '@/styles/popoverMixins';
import { getTransferInputs } from '@/state/inputsSelectors';
import { isTruthy } from '@/lib/isTruthy';
import { testFlags } from '@/lib/testFlags';
type ElementProps = {
label?: string;
selectedExchange?: string;
selectedChain?: string;
onSelectChain: (chain: string) => void;
onSelect: (name: string, type: 'chain' | 'exchange') => void;
};
export const ChainSelectMenu = ({ label, selectedChain, onSelectChain }: ElementProps) => {
export const SourceSelectMenu = ({
label,
selectedExchange,
selectedChain,
onSelect,
}: ElementProps) => {
const stringGetter = useStringGetter();
const { type, depositOptions, withdrawalOptions, resources } =
useSelector(getTransferInputs, shallowEqual) || {};
const chains =
(type === TransferType.deposit ? depositOptions : withdrawalOptions)?.chains?.toArray() || [];
const exchanges =
(type === TransferType.deposit ? depositOptions : withdrawalOptions)?.exchanges?.toArray() ||
[];
const chainItems = Object.values(chains).map((chain) => ({
value: chain.type,
label: chain.stringKey,
onSelect: () => {
onSelectChain(chain.type);
onSelect(chain.type, 'chain');
},
slotBefore: <Styled.Img src={chain.iconUrl} alt="" />,
}));
const selectedOption = chains.find((item) => item.type === selectedChain);
const exchangeItems = Object.values(exchanges).map((exchange) => ({
value: exchange.type,
label: exchange.string,
onSelect: () => {
onSelect(exchange.type, 'exchange');
},
slotBefore: <Styled.Img src={exchange.iconUrl} alt="" />,
}));
const selectedChainOption = chains.find((item) => item.type === selectedChain);
const selectedExchangeOption = exchanges.find((item) => item.type === selectedExchange);
return (
<SearchSelectMenu
items={[
{
exchangeItems.length > 0 && testFlags.showCEXDepositOption && {
group: 'exchanges',
groupLabel: stringGetter({ key: STRING_KEYS.EXCHANGES }),
items: exchangeItems,
},
chainItems.length > 0 && {
group: 'chains',
groupLabel: stringGetter({ key: STRING_KEYS.CHAINS }),
items: chainItems,
},
]}
].filter(isTruthy)}
label={label || (type === TransferType.deposit ? 'Source' : 'Destination')}
>
<Styled.ChainRow>
{selectedChain ? (
{selectedChainOption ? (
<>
<Styled.Img src={selectedOption?.iconUrl} alt="" /> {selectedOption?.stringKey}
<Styled.Img src={selectedChainOption.iconUrl} alt="" /> {selectedChainOption.stringKey}
</>
) : selectedExchangeOption ? (
<>
<Styled.Img src={selectedExchangeOption.iconUrl} alt="" />{' '}
{selectedExchangeOption.string}
</>
) : (
stringGetter({ key: STRING_KEYS.SELECT_CHAIN })

View File

@ -38,7 +38,7 @@ import { Tag } from '@/components/Tag';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { Icon, IconName } from '@/components/Icon';
import { ChainSelectMenu } from '@/views/forms/AccountManagementForms/ChainSelectMenu';
import { SourceSelectMenu } from '@/views/forms/AccountManagementForms/SourceSelectMenu';
import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
@ -374,10 +374,10 @@ export const WithdrawForm = () => {
</span>
}
/>
<ChainSelectMenu
<SourceSelectMenu
label={stringGetter({ key: STRING_KEYS.NETWORK })}
selectedChain={chainIdStr || undefined}
onSelectChain={onSelectChain}
onSelect={onSelectChain}
/>
</Styled.DestinationRow>
<TokenSelectMenu selectedToken={toToken || undefined} onSelectToken={onSelectToken} />

View File

@ -0,0 +1,131 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { OpacityToken } from '@/constants/styles/base';
import { STRING_KEYS } from '@/constants/localization';
import { layoutMixins } from '@/styles/layoutMixins';
import { useAccounts, useStringGetter } from '@/hooks';
import { CopyButton } from '@/components/CopyButton';
import { QrCode } from '@/components/QrCode';
import { Checkbox } from '@/components/Checkbox';
import { Icon, IconName } from '@/components/Icon';
import { TimeoutButton } from '@/components/TimeoutButton';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { WithReceipt } from '@/components/WithReceipt';
import { generateFadedColorVariant } from '@/lib/styles';
export const NobleDeposit = () => {
const [hasAcknowledged, setHasAcknowledged] = useState(false);
const stringGetter = useStringGetter();
const { nobleAddress } = useAccounts();
return (
<>
<WithDetailsReceipt
side="bottom"
detailItems={[
{
key: 'nobleAddress',
label: stringGetter({ key: STRING_KEYS.NOBLE_ADDRESS }),
value: nobleAddress,
},
]}
>
<Styled.QrCodeContainer>
<Styled.QrCode size={432} value={nobleAddress || ''} />
</Styled.QrCodeContainer>
</WithDetailsReceipt>
<Styled.WaitingSpan>
<Styled.CautionIconContainer>
<Icon iconName={IconName.CautionCircleStroked} />
</Styled.CautionIconContainer>
<p>{stringGetter({ key: STRING_KEYS.NOBLE_WARNING })}</p>
</Styled.WaitingSpan>
<Styled.WithReceipt
slotReceipt={
<Styled.CheckboxContainer>
<Checkbox
checked={hasAcknowledged}
onCheckedChange={setHasAcknowledged}
id="acknowledge-secret-phase-risk"
label={stringGetter({
key: STRING_KEYS.NOBLE_ACKNOWLEDGEMENT,
})}
/>
</Styled.CheckboxContainer>
}
>
<TimeoutButton
timeoutInSeconds={8}
slotFinal={<CopyButton state={{ isDisabled: !hasAcknowledged }} value={nobleAddress} />}
/>
</Styled.WithReceipt>
</>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.WaitingSpan = styled.span`
${layoutMixins.row}
gap: 1rem;
color: var(--color-text-1);
`;
Styled.WithReceipt = styled(WithReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.QrCodeContainer = styled.div`
display: flex;
justify-content: center;
padding: 0.5rem;
background-color: var(--color-layer-2);
border-radius: 0.5rem;
`;
Styled.QrCode = styled(QrCode)`
max-height: 20rem;
width: fit-content;
svg {
max-height: 20rem;
}
`;
Styled.CheckboxContainer = styled.div`
padding: 1rem;
color: var(--color-text-0);
`;
Styled.CautionIconContainer = styled.div`
${layoutMixins.stack}
min-width: 2.5rem;
height: 2.5rem;
align-items: center;
border-radius: 50%;
overflow: hidden;
color: var(--color-warning);
svg {
width: 1.125em;
height: 1.125em;
justify-self: center;
}
&:before {
content: '';
width: 2.5rem;
height: 2.5rem;
background-color: ${({ theme }) =>
generateFadedColorVariant(theme.warning, OpacityToken.Opacity16)};
}
`;