Compare commits

...

6 Commits

Author SHA1 Message Date
Bill He
466cd85502
fix chain select 2024-01-29 15:05:22 -08:00
Bill He
4ced747e8d
Address feedback 2024-01-29 14:56:48 -08:00
Bill He
8e1f497f43
add testflag 2024-01-26 14:14:49 -08:00
Bill He
a4750921d6
fix lock 2024-01-26 14:12:06 -08:00
Bill He
51d056d346
bump packages 2024-01-26 14:10:52 -08:00
Bill He
103d1718ca
Coinbase Deposit 2024-01-26 14:09:44 -08:00
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 = { type StyleProps = {
className?: string;
hasLogo?: boolean; hasLogo?: boolean;
size?: number; size?: number;
}; };
@ -18,7 +19,7 @@ type StyleProps = {
const DARK_LOGO_MARK_URL = '/logos/logo-mark-dark.svg'; const DARK_LOGO_MARK_URL = '/logos/logo-mark-dark.svg';
const LIGHT_LOGO_MARK_URL = '/logos/logo-mark-light.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 ref = useRef<HTMLDivElement>(null);
const appTheme: AppTheme = useSelector(getAppTheme); const appTheme: AppTheme = useSelector(getAppTheme);
@ -74,7 +75,7 @@ export const QrCode = ({ value, hasLogo, size = 300 }: ElementProps & StyleProps
} }
}, [appTheme, hasLogo]); }, [appTheme, hasLogo]);
return <Styled.QrCode ref={ref} />; return <Styled.QrCode className={className} ref={ref} />;
}; };
const Styled: Record<string, AnyStyledComponent> = {}; const Styled: Record<string, AnyStyledComponent> = {};

View File

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

View File

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

View File

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

View File

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

View File

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