Compare commits

...

8 Commits

Author SHA1 Message Date
Bill He
849bda8e8e
privy theme 2024-01-18 21:22:44 -08:00
Bill He
57edb711d5
PWA 2024-01-18 19:55:43 -08:00
Bill He
8235c64c93
Noble deposit 2024-01-18 19:10:16 -08:00
Bill He
750fb077f5
remove debugs 2024-01-18 12:36:10 -08:00
Bill He
d781cb3199
conditions 2024-01-18 12:28:57 -08:00
Bill He
f6bf635b0d
debug 2024-01-18 12:21:47 -08:00
Bill He
56187f7a84
ignore ready 2024-01-18 12:18:44 -08:00
Bill He
93c76f1c3f
Privy integration [WIP] 2024-01-18 11:58:21 -08:00
15 changed files with 2920 additions and 64 deletions

1
dev-dist/registerSW.js Normal file
View File

@ -0,0 +1 @@
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })

View File

@ -44,6 +44,8 @@
"@dydxprotocol/v4-localization": "^1.1.11",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@privy-io/react-auth": "^1.53.1",
"@privy-io/wagmi-connector": "^0.1.12",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
@ -151,6 +153,7 @@
"url-polyfill": "^1.1.12",
"util": "^0.12.5",
"vite": "^4.3.9",
"vite-plugin-pwa": "^0.17.4",
"vite-plugin-svgr": "^3.2.0",
"vitest": "^0.32.2",
"w3name": "^1.0.8",

2754
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,8 @@ import { lazy, Suspense } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import styled, { AnyStyledComponent, css } from 'styled-components';
import { WagmiConfig } from 'wagmi';
import { PrivyProvider } from '@privy-io/react-auth';
import { PrivyWagmiConnector } from '@privy-io/wagmi-connector';
import { QueryClient, QueryClientProvider } from 'react-query';
import { GrazProvider } from 'graz';
@ -32,7 +34,7 @@ import { NotificationsToastArea } from '@/layout/NotificationsToastArea';
import { DialogManager } from '@/layout/DialogManager';
import { GlobalCommandDialog } from '@/views/dialogs/GlobalCommandDialog';
import { config } from '@/lib/wagmi';
import { config, privyConfig } from '@/lib/wagmi';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
@ -120,8 +122,13 @@ const wrapProvider = (Component: React.ComponentType<any>, props?: any) => {
};
const providers = [
wrapProvider(PrivyProvider, {
appId: import.meta.env.VITE_PRIVY_APP_ID,
config: privyConfig,
}),
wrapProvider(QueryClientProvider, { client: queryClient }),
wrapProvider(GrazProvider),
wrapProvider(PrivyWagmiConnector, { wagmiChainsConfig: config }),
wrapProvider(WagmiConfig, { config }),
wrapProvider(LocaleProvider),
wrapProvider(RestrictionProvider),

View File

@ -31,6 +31,7 @@ import { DydxNetwork, ENVIRONMENT_CONFIG_MAP } from './networks';
export enum WalletConnectionType {
CoinbaseWalletSdk = 'coinbaseWalletSdk',
CosmosSigner = 'CosmosSigner',
Privy = 'Privy',
InjectedEip1193 = 'injectedEip1193',
WalletConnect2 = 'walletConnect2',
}
@ -68,6 +69,9 @@ export const walletConnectionTypes: Record<WalletConnectionType, WalletConnectio
[WalletConnectionType.CosmosSigner]: {
name: 'CosmosSigner',
},
[WalletConnectionType.Privy]: {
name: 'Privy',
},
};
// Wallets
@ -90,6 +94,7 @@ export enum WalletType {
WalletConnect2 = 'WALLETCONNECT_2',
// TestWallet = 'TEST_WALLET',
OtherWallet = 'OTHER_WALLET',
Privy = 'PRIVY',
}
const WALLET_CONNECT_EXPLORER_RECOMMENDED_WALLETS = {
@ -243,6 +248,12 @@ export const wallets: Record<WalletType, WalletConfig> = {
icon: KeplrIcon,
connectionTypes: [WalletConnectionType.CosmosSigner],
},
[WalletType.Privy]: {
type: WalletType.Privy,
stringKey: STRING_KEYS.KEPLR,
icon: GenericWalletIcon,
connectionTypes: [WalletConnectionType.Privy],
},
};
// Injected EIP-1193 Providers

View File

@ -1,13 +1,19 @@
import { useCallback, useContext, createContext, useEffect, useState, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { AES, enc } from 'crypto-js';
import { NOBLE_BECH32_PREFIX, LocalWallet, type Subaccount } from '@dydxprotocol/v4-client-js';
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs';
import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage';
import { DydxAddress, EvmAddress, PrivateInformation } from '@/constants/wallets';
import {
DydxAddress,
EvmAddress,
PrivateInformation,
WalletConnectionType,
getSignTypedData,
} from '@/constants/wallets';
import { setOnboardingState, setOnboardingGuard } from '@/state/account';
import { forceOpenDialog } from '@/state/dialogs';
@ -20,6 +26,11 @@ import { useLocalStorage } from './useLocalStorage';
import { useRestrictions } from './useRestrictions';
import { useWalletConnection } from './useWalletConnection';
import { useSignTypedData } from 'wagmi';
import { getSelectedNetwork } from '@/state/appSelectors';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { usePrivy } from '@privy-io/react-auth';
const AccountsContext = createContext<ReturnType<typeof useAccountsContext> | undefined>(undefined);
AccountsContext.displayName = 'Accounts';
@ -153,6 +164,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]);
@ -162,6 +174,11 @@ const useAccountsContext = () => {
[localDydxWallet]
);
const nobleAddress = useMemo(
() => localNobleWallet?.address,
[localNobleWallet]
);
const setWalletFromEvmSignature = async (signature: string) => {
const { wallet, mnemonic, privateKey, publicKey } = await getWalletFromEvmSignature({
signature,
@ -176,6 +193,20 @@ const useAccountsContext = () => {
}
}, [evmAddress, dydxAddress]);
const selectedNetwork = useSelector(getSelectedNetwork);
const chainId = Number(ENVIRONMENT_CONFIG_MAP[selectedNetwork].ethereumChainId);
const signTypedData = getSignTypedData(selectedNetwork);
const { signTypedDataAsync } = useSignTypedData({
...signTypedData,
domain: {
...signTypedData.domain,
chainId,
},
});
const { ready, authenticated } = usePrivy();
useEffect(() => {
(async () => {
if (connectedDydxAddress && signerGraz) {
@ -192,7 +223,17 @@ const useAccountsContext = () => {
const evmDerivedAccount = evmDerivedAddresses[evmAddress];
if (evmDerivedAccount?.encryptedSignature) {
if (walletConnectionType === WalletConnectionType.Privy && authenticated && ready) {
try {
const signature = await signTypedDataAsync();
await setWalletFromEvmSignature(signature);
dispatch(setOnboardingState(OnboardingState.AccountConnected));
} catch (error) {
log('useAccounts/decryptSignature', error);
forgetEvmSignature();
}
} else if (evmDerivedAccount?.encryptedSignature) {
try {
const signature = decryptSignature(evmDerivedAccount.encryptedSignature);
@ -211,7 +252,15 @@ const useAccountsContext = () => {
dispatch(setOnboardingState(OnboardingState.Disconnected));
}
})();
}, [evmAddress, evmDerivedAddresses, signerWagmi, connectedDydxAddress, signerGraz]);
}, [
evmAddress,
evmDerivedAddresses,
signerWagmi,
connectedDydxAddress,
signerGraz,
authenticated,
ready,
]);
// abacus
useEffect(() => {
@ -224,6 +273,7 @@ const useAccountsContext = () => {
if (hdKey?.mnemonic) {
const nobleWallet = await LocalWallet.fromMnemonic(hdKey.mnemonic, NOBLE_BECH32_PREFIX);
abacusStateManager.setNobleWallet(nobleWallet);
setLocalNobleWallet(nobleWallet);
}
};
setNobleWallet();
@ -322,6 +372,7 @@ const useAccountsContext = () => {
localDydxWallet,
dydxAccounts,
dydxAddress,
nobleAddress,
// Onboarding state
saveHasAcknowledgedTerms,

View File

@ -21,6 +21,7 @@ export const useDisplayedWallets = () => {
// WalletType.BitKeep,
// WalletType.Coin98,
WalletType.Privy,
WalletType.OtherWallet,
].filter(isTruthy);
};

View File

@ -1,5 +1,9 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useNetwork, useSwitchNetwork } from 'wagmi';
import { useSwitchNetwork as useSwitchNetworkPrivy } from '@privy-io/wagmi-connector';
import { WalletConnectionType } from '@/constants/wallets';
import { useWalletConnection } from './useWalletConnection';
export const useMatchingEvmNetwork = ({
chainId,
@ -11,7 +15,10 @@ export const useMatchingEvmNetwork = ({
onError?: (error: Error) => void;
}) => {
const { chain } = useNetwork();
const { walletConnectionType } = useWalletConnection();
const { isLoading, switchNetworkAsync } = useSwitchNetwork({ onError });
const { isLoading: isLoadingPrivy, switchNetworkAsync: switchNetworkAsyncPrivy } =
useSwitchNetworkPrivy({ onError });
// If chainId is not a number, we can assume it is a non EVM compatible chain
const isMatchingNetwork = useMemo(
@ -21,7 +28,11 @@ export const useMatchingEvmNetwork = ({
const matchNetwork = useCallback(async () => {
if (!isMatchingNetwork) {
await switchNetworkAsync?.(Number(chainId));
if (walletConnectionType === WalletConnectionType.Privy) {
await switchNetworkAsyncPrivy?.(Number(chainId));
} else {
await switchNetworkAsync?.(Number(chainId));
}
}
}, [chainId, chain]);
@ -34,6 +45,6 @@ export const useMatchingEvmNetwork = ({
return {
isMatchingNetwork,
matchNetwork,
isSwitchingNetwork: isLoading,
isSwitchingNetwork: isLoading || isLoadingPrivy,
};
};

View File

@ -1,5 +1,6 @@
import { useCallback, useEffect, useState, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { usePrivy, useLogout } from '@privy-io/react-auth';
import { LocalStorageKey } from '@/constants/localStorage';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
@ -104,8 +105,10 @@ export const useWalletConnection = () => {
[walletConnectConfig, walletType, walletConnectionType]
);
const { connectAsync: connectWagmi } = useConnectWagmi({ connector: wagmiConnector })
const { connectAsync: connectWagmi } = useConnectWagmi({ connector: wagmiConnector });
const { suggestAndConnect: connectGraz } = useConnectGraz();
const { login, ready, authenticated } = usePrivy();
const { logout } = useLogout();
const connectWallet = useCallback(
async ({ walletType }: { walletType: WalletType }) => {
@ -114,6 +117,11 @@ export const useWalletConnection = () => {
try {
if (!walletConnection) {
throw new Error('Onboarding: No wallet connection found.');
} else if (walletConnection.type === WalletConnectionType.Privy) {
if (!isConnectedWagmi && !authenticated && ready) {
login();
}
} else if (walletConnection.type === WalletConnectionType.CosmosSigner) {
const cosmosWalletType = {
[WalletType.Keplr as string]: CosmosWalletType.KEPLR,
@ -156,7 +164,7 @@ export const useWalletConnection = () => {
walletConnectionType: walletConnection.type,
};
},
[isConnectedGraz, signerGraz, isConnectedWagmi, signerWagmi]
[isConnectedGraz, signerGraz, isConnectedWagmi, signerWagmi, login, ready, authenticated]
);
const disconnectWallet = useCallback(async () => {
@ -165,6 +173,7 @@ export const useWalletConnection = () => {
if (isConnectedWagmi) await disconnectWagmi();
if (isConnectedGraz) await disconnectGraz();
if (authenticated) await logout();
}, [isConnectedGraz, isConnectedWagmi]);
// Wallet selection

View File

@ -1,5 +1,6 @@
import { createConfig, configureChains, mainnet, Chain } from 'wagmi';
import { goerli } from 'wagmi/chains';
import type { PrivyClientConfig } from '@privy-io/react-auth';
import {
arbitrum,
@ -85,6 +86,19 @@ export const WAGMI_SUPPORTED_CHAINS: Chain[] = [
kava,
];
export const privyConfig: PrivyClientConfig = {
embeddedWallets: {
createOnLogin: 'users-without-wallets',
requireUserPasswordOnCreate: true,
noPromptOnSignature: true,
},
loginMethods: ['email', 'sms', 'twitter', 'google', 'apple'],
appearance: {
theme: '#28283c',
},
defaultChain: sepolia
};
const { chains, publicClient, webSocketPublicClient } = configureChains(
WAGMI_SUPPORTED_CHAINS,
[

View File

@ -62,6 +62,12 @@ export const getWalletConnection = ({
type: WalletConnectionType.CosmosSigner,
};
}
case WalletConnectionType.Privy: {
return {
type: WalletConnectionType.Privy,
};
}
}
}
};

View File

@ -1,6 +1,4 @@
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';
@ -13,7 +11,7 @@ import { Button } from '@/components/Button';
import { Icon } from '@/components/Icon';
import { Link } from '@/components/Link';
import { useAccounts, useStringGetter, useURLConfigs } from '@/hooks';
import { useAccounts, useBreakpoints, useStringGetter, useURLConfigs } from '@/hooks';
import { useDisplayedWallets } from '@/hooks/useDisplayedWallets';
import { breakpoints } from '@/styles';
@ -27,6 +25,8 @@ export const ChooseWallet = () => {
const { selectWalletType, selectedWalletType, selectedWalletError } = useAccounts();
const { isMobile } = useBreakpoints();
return (
<>
{selectedWalletType && selectedWalletError && (
@ -56,7 +56,11 @@ export const ChooseWallet = () => {
slotLeft={<Styled.Icon iconComponent={wallets[walletType].icon} />}
size={ButtonSize.Small}
>
<div>{stringGetter({ key: wallets[walletType].stringKey })}</div>
<div>
{walletType !== WalletType.Privy && import.meta.env.VITE_PRIVY_APP_ID
? stringGetter({ key: wallets[walletType].stringKey })
: `Socials ${isMobile ? '' : '(Email, SMS, etc.)'}`}
</div>
</Styled.WalletButton>
))}
</Styled.Wallets>

View File

@ -34,11 +34,30 @@ export const ChainSelectMenu = ({ label, selectedChain, onSelectChain }: Element
slotBefore: <Styled.Img src={chain.iconUrl} alt="" />,
}));
const selectedOption = chains.find((item) => item.type === selectedChain);
const selectedOption = selectedChain === 'coinbase' ? {
stringKey: 'Coinbase',
iconUrl: '/wallets/coinbase-wallet.png',
} : chains.find((item) => item.type === selectedChain);
const exchanges = [
{
value: 'coinbase',
label: 'Coinbase',
onSelect: () => {
onSelectChain('coinbase');
},
slotBefore: <Styled.Img src="/wallets/coinbase-wallet.png" alt="" />,
}
];
return (
<SearchSelectMenu
items={[
{
group: 'exchanges',
groupLabel: 'Exchanges',
items: exchanges,
},
{
group: 'chains',
groupLabel: stringGetter({ key: STRING_KEYS.CHAINS }),

View File

@ -38,12 +38,14 @@ import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { getNobleChainId, NATIVE_TOKEN_ADDRESS } from '@/lib/squid';
import { log } from '@/lib/telemetry';
import { parseWalletError } from '@/lib/wallet';
import { parseWalletError, truncateAddress } from '@/lib/wallet';
import { ChainSelectMenu } from './ChainSelectMenu';
import { TokenSelectMenu } from './TokenSelectMenu';
import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt';
import { QrCode } from '@/components/QrCode';
import { CopyButton } from '@/components/CopyButton';
type DepositFormProps = {
onDeposit?: () => void;
@ -55,8 +57,9 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { selectedNetwork } = useSelectedNetwork();
const [isCoinbase, setIsCoinbase] = useState(false);
const { evmAddress, signerWagmi, publicClientWagmi } = useAccounts();
const { evmAddress, signerWagmi, publicClientWagmi, nobleAddress } = useAccounts();
const { addTransferNotification } = useLocalNotifications();
@ -133,7 +136,10 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
}, [error]);
const onSelectChain = useCallback((chain: string) => {
if (chain) {
if (chain === 'coinbase') {
setIsCoinbase(true);
} else if (chain) {
setIsCoinbase(false);
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
@ -364,9 +370,43 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
return <LoadingSpace id="DepositForm" />;
}
if (isCoinbase) {
return (
<Styled.Form onSubmit={onSubmit}>
<ChainSelectMenu
selectedChain={isCoinbase ? 'coinbase' : chainIdStr || undefined}
onSelectChain={onSelectChain}
/>
{nobleAddress && (
<>
<Styled.ReceiveQr
side="bottom"
detailItems={[
{
key: 'nobleAddress',
label: "Noble Address",
value: nobleAddress,
},
]}
>
<QrCode size={432} value={nobleAddress} />
</Styled.ReceiveQr>
<p>
* This address only supports transfers of USDC to the Noble chain.
</p>
<CopyButton value={nobleAddress} />
</>
)}
</Styled.Form>
);
}
return (
<Styled.Form onSubmit={onSubmit}>
<ChainSelectMenu selectedChain={chainIdStr || undefined} onSelectChain={onSelectChain} />
<ChainSelectMenu
selectedChain={isCoinbase ? 'coinbase' : chainIdStr || undefined}
onSelectChain={onSelectChain}
/>
<TokenSelectMenu selectedToken={sourceToken || undefined} onSelectToken={onSelectToken} />
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<FormInput
@ -427,3 +467,13 @@ Styled.TransactionInfo = styled.span`
Styled.FormInputButton = styled(Button)`
${formMixins.inputInnerButton}
`;
Styled.Exchange = styled.div`
${layoutMixins.flexColumn}
gap: 1rem;
align-items: center;
`;
Styled.ReceiveQr = styled(WithDetailsReceipt)`
`;

View File

@ -2,6 +2,7 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import svgr from 'vite-plugin-svgr';
import { VitePWA } from 'vite-plugin-pwa';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
@ -50,6 +51,12 @@ export default defineConfig(({ mode }) => ({
svgr({
exportAsDefault: true,
}),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
enabled: true,
},
}),
],
publicDir: 'public',
}));