diff --git a/apps/web/src/components/WalletSelector/multichain/EthereumAddWallet.tsx b/apps/web/src/components/WalletSelector/multichain/EthereumAddWallet.tsx index 9f57798ae8..96f8e7157b 100644 --- a/apps/web/src/components/WalletSelector/multichain/EthereumAddWallet.tsx +++ b/apps/web/src/components/WalletSelector/multichain/EthereumAddWallet.tsx @@ -3,6 +3,7 @@ import { signMessage } from '@wagmi/core'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFragment } from 'react-relay'; import { graphql } from 'relay-runtime'; +import { useGetUserByWalletAddressImperatively } from 'shared/hooks/useGetUserByWalletAddress'; import styled from 'styled-components'; import { useAccount } from 'wagmi'; @@ -80,6 +81,7 @@ export const EthereumAddWallet = ({ queryRef, reset, onSuccess = noop }: Props) const { hideModal } = useModalActions(); const createNonce = useCreateNonce(); + const getUserByWalletAddress = useGetUserByWalletAddressImperatively(); const trackAddWalletAttempt = useTrackAddWalletAttempt(); const trackAddWalletSuccess = useTrackAddWalletSuccess(); const trackAddWalletError = useTrackAddWalletError(); @@ -98,19 +100,23 @@ export const EthereumAddWallet = ({ queryRef, reset, onSuccess = noop }: Props) setPendingState(PROMPT_SIGNATURE); trackAddWalletAttempt('Ethereum'); - const { nonce, user_exists: userExists } = await createNonce(address, 'Ethereum'); + + const userExists = Boolean(await getUserByWalletAddress({ address, chain: 'Ethereum' })); if (userExists) { throw { code: 'EXISTING_USER' } as Web3Error; } - const signature = await signMessage({ message: nonce }); + const { nonce, message } = await createNonce(); + + const signature = await signMessage({ message }); const { signatureValid } = await addWallet({ authMechanism: { eoa: { - signature, nonce, + message, + signature, chainPubKey: { pubKey: address, chain: 'Ethereum', @@ -149,12 +155,13 @@ export const EthereumAddWallet = ({ queryRef, reset, onSuccess = noop }: Props) }, [ trackAddWalletAttempt, + getUserByWalletAddress, createNonce, addWallet, - hideModal, trackAddWalletSuccess, - trackAddWalletError, + hideModal, onSuccess, + trackAddWalletError, ] ); diff --git a/apps/web/src/components/WalletSelector/multichain/EthereumAuthenticateWallet.tsx b/apps/web/src/components/WalletSelector/multichain/EthereumAuthenticateWallet.tsx index d189eadbb6..51040f1d81 100644 --- a/apps/web/src/components/WalletSelector/multichain/EthereumAuthenticateWallet.tsx +++ b/apps/web/src/components/WalletSelector/multichain/EthereumAuthenticateWallet.tsx @@ -1,6 +1,7 @@ import { captureException } from '@sentry/nextjs'; import { signMessage } from '@wagmi/core'; import { useCallback, useEffect, useState } from 'react'; +import { useGetUserByWalletAddressImperatively } from 'shared/hooks/useGetUserByWalletAddress'; import { useAccount } from 'wagmi'; import { EmptyState } from '~/components/EmptyState/EmptyState'; @@ -28,6 +29,7 @@ export const EthereumAuthenticateWallet = ({ reset }: Props) => { const [pendingState, setPendingState] = useState(INITIAL); const createNonce = useCreateNonce(); + const getUserByWalletAddress = useGetUserByWalletAddressImperatively(); const [loginOrRedirectToOnboarding] = useLoginOrRedirectToOnboarding(); const trackSignInAttempt = useTrackSignInAttempt(); @@ -46,9 +48,11 @@ export const EthereumAuthenticateWallet = ({ reset }: Props) => { setPendingState(PROMPT_SIGNATURE); trackSignInAttempt('Ethereum'); - const { nonce, user_exists: userExists } = await createNonce(address, 'Ethereum'); + const { nonce, message } = await createNonce(); - const signature = await signMessage({ message: nonce }); + const signature = await signMessage({ message }); + + const userExists = Boolean(await getUserByWalletAddress({ address, chain: 'Ethereum' })); const userId = await loginOrRedirectToOnboarding({ authMechanism: { @@ -59,6 +63,7 @@ export const EthereumAuthenticateWallet = ({ reset }: Props) => { pubKey: address, }, nonce, + message, signature, }, }, @@ -70,7 +75,13 @@ export const EthereumAuthenticateWallet = ({ reset }: Props) => { trackSignInSuccess('Ethereum'); } }, - [trackSignInAttempt, createNonce, loginOrRedirectToOnboarding, trackSignInSuccess] + [ + trackSignInAttempt, + createNonce, + getUserByWalletAddress, + loginOrRedirectToOnboarding, + trackSignInSuccess, + ] ); useEffect(() => { diff --git a/apps/web/src/components/WalletSelector/multichain/GnosisSafeAddWallet.tsx b/apps/web/src/components/WalletSelector/multichain/GnosisSafeAddWallet.tsx index 73b348b663..8f9c089a1e 100644 --- a/apps/web/src/components/WalletSelector/multichain/GnosisSafeAddWallet.tsx +++ b/apps/web/src/components/WalletSelector/multichain/GnosisSafeAddWallet.tsx @@ -3,6 +3,7 @@ import { captureException } from '@sentry/nextjs'; import { useWeb3React } from '@web3-react/core'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { graphql, useFragment } from 'react-relay'; +import { useGetUserByWalletAddressImperatively } from 'shared/hooks/useGetUserByWalletAddress'; import { VStack } from '~/components/core/Spacer/Stack'; import { BaseM } from '~/components/core/Text/Text'; @@ -56,6 +57,7 @@ export const GnosisSafeAddWallet = ({ queryRef, reset }: Props) => { const previousAttemptNonce = useMemo(() => getLocalStorageItem(GNOSIS_NONCE_STORAGE_KEY), []); const [nonce, setNonce] = useState(''); + const [message, setMessage] = useState(''); const [userExists, setUserExists] = useState(false); const [authenticationFlowStarted, setAuthenticationFlowStarted] = useState(false); const [error, setError] = useState(); @@ -127,7 +129,7 @@ export const GnosisSafeAddWallet = ({ queryRef, reset }: Props) => { const addWallet = useAddWallet(); const authenticateWithBackend = useCallback( - async (address: string, nonce: string) => { + async ({ address, nonce, message }: { address: string; nonce: string; message: string }) => { const { signatureValid } = await addWallet({ chainAddress: { address, @@ -137,6 +139,7 @@ export const GnosisSafeAddWallet = ({ queryRef, reset }: Props) => { gnosisSafe: { address, nonce, + message, }, }, }); @@ -154,7 +157,17 @@ export const GnosisSafeAddWallet = ({ queryRef, reset }: Props) => { // Initiates the full authentication flow including signing the message, listening for the signature, validating it. then calling the backend // This is the default flow const attemptAddWallet = useCallback( - async (address: string, nonce: string, userExists: boolean) => { + async ({ + address, + nonce, + message, + userExists, + }: { + address: string; + nonce: string; + message: string; + userExists: boolean; + }) => { try { if (userExists) { throw { code: 'EXISTING_USER' } as Web3Error; @@ -162,13 +175,17 @@ export const GnosisSafeAddWallet = ({ queryRef, reset }: Props) => { setPendingState(PROMPT_SIGNATURE); trackAddWalletAttempt('Gnosis Safe'); - await signMessageWithContractAccount(address, nonce, walletconnect); - window.localStorage.setItem(GNOSIS_NONCE_STORAGE_KEY, JSON.stringify(nonce)); + await signMessageWithContractAccount(address, message, walletconnect); + window.localStorage.setItem(GNOSIS_NONCE_STORAGE_KEY, JSON.stringify(message)); setPendingState(LISTENING_ONCHAIN); - await listenForGnosisSignature(address, nonce, walletconnect); + await listenForGnosisSignature(address, message, walletconnect); - await authenticateWithBackend(address, nonce); + await authenticateWithBackend({ + address, + nonce, + message, + }); } catch (error) { handleError(error); } @@ -187,30 +204,36 @@ export const GnosisSafeAddWallet = ({ queryRef, reset }: Props) => { try { // Immediately check if the message has already been signed and executed on chain - const wasSigned = await validateNonceSignedByGnosis(account, nonce, walletconnect); + const wasSigned = await validateNonceSignedByGnosis(account, message, walletconnect); if (wasSigned) { - await authenticateWithBackend(account, nonce); + await authenticateWithBackend({ address: account.toLowerCase(), nonce, message }); } // If it hasn't, set up a listener because the transaction may not have been executed yet if (pendingState !== LISTENING_ONCHAIN) { setPendingState(LISTENING_ONCHAIN); - await listenForGnosisSignature(account, nonce, walletconnect); + await listenForGnosisSignature(account, message, walletconnect); // Once signed, call the backend as usual - void authenticateWithBackend(account, nonce); + void authenticateWithBackend({ address: account.toLowerCase(), nonce, message }); } } catch (error) { handleError(error); } - }, [account, authenticateWithBackend, handleError, pendingState, nonce]); + }, [account, nonce, pendingState, authenticateWithBackend, message, handleError]); const restartAuthentication = useCallback(async () => { if (account) { - await attemptAddWallet(account.toLowerCase(), nonce, userExists); + await attemptAddWallet({ + address: account.toLowerCase(), + nonce, + message, + userExists, + }); } - }, [account, attemptAddWallet, nonce, userExists]); + }, [account, attemptAddWallet, message, nonce, userExists]); const createNonce = useCreateNonce(); + const getUserByWalletAddress = useGetUserByWalletAddressImperatively(); // This runs once to auto-initiate the authentication flow, when wallet is first connected (ie when 'account' is defined) useEffect(() => { @@ -222,20 +245,32 @@ export const GnosisSafeAddWallet = ({ queryRef, reset }: Props) => { setAuthenticationFlowStarted(true); const account = await connectGnosisSafe(); - if (authenticatedUserAddresses.includes(account.toLowerCase())) { + const address = account.toLowerCase(); + if (authenticatedUserAddresses.includes(address)) { setPendingState(ADDRESS_ALREADY_CONNECTED); return; } - const { nonce, user_exists: userExists } = await createNonce(account, 'Ethereum'); + const { nonce, message } = await createNonce(); + + const userExistsOnGallery = Boolean( + await getUserByWalletAddress({ address, chain: 'Ethereum' }) + ); + setNonce(nonce); - setUserExists(userExists); + setMessage(message); + setUserExists(userExistsOnGallery); if (nonce === previousAttemptNonce) { return; } - await attemptAddWallet(account.toLowerCase(), nonce, userExists); + await attemptAddWallet({ + address, + nonce, + message, + userExists: userExistsOnGallery, + }); } void initiateAuthentication().catch(handleError); @@ -247,6 +282,7 @@ export const GnosisSafeAddWallet = ({ queryRef, reset }: Props) => { handleError, previousAttemptNonce, createNonce, + getUserByWalletAddress, ]); if (error) { diff --git a/apps/web/src/components/WalletSelector/mutations/useLoginOrRedirectToOnboarding.ts b/apps/web/src/components/WalletSelector/mutations/useLoginOrRedirectToOnboarding.ts index 63671a5b9a..359558cb98 100644 --- a/apps/web/src/components/WalletSelector/mutations/useLoginOrRedirectToOnboarding.ts +++ b/apps/web/src/components/WalletSelector/mutations/useLoginOrRedirectToOnboarding.ts @@ -86,6 +86,7 @@ export default function useLoginOrRedirectToOnboarding() { chain: authMechanism.mechanism.eoa.chainPubKey.chain, address: authMechanism.mechanism.eoa.chainPubKey.pubKey, nonce: authMechanism.mechanism.eoa.nonce, + message: authMechanism.mechanism.eoa.message, signature: authMechanism.mechanism.eoa.signature, userFriendlyWalletName, }, @@ -99,6 +100,7 @@ export default function useLoginOrRedirectToOnboarding() { authMechanismType: 'gnosisSafe', address: authMechanism.mechanism.gnosisSafe.address, nonce: authMechanism.mechanism.gnosisSafe.nonce, + message: authMechanism.mechanism.gnosisSafe.message, userFriendlyWalletName, }, }); diff --git a/packages/shared/src/hooks/useCreateNonce.ts b/packages/shared/src/hooks/useCreateNonce.ts index f8ac9aac8f..e548977f54 100644 --- a/packages/shared/src/hooks/useCreateNonce.ts +++ b/packages/shared/src/hooks/useCreateNonce.ts @@ -1,10 +1,9 @@ import { useCallback } from 'react'; import { graphql } from 'relay-runtime'; -import { Chain, useCreateNonceMutation } from '~/generated/useCreateNonceMutation.graphql'; +import { useCreateNonceMutation } from '~/generated/useCreateNonceMutation.graphql'; import { usePromisifiedMutation } from '../relay/usePromisifiedMutation'; -import { Web3Error } from '../utils/Error'; /** * Retrieve a nonce for the client to sign given a wallet address. @@ -13,75 +12,52 @@ import { Web3Error } from '../utils/Error'; */ type NonceResponse = { nonce: string; - user_exists: boolean; + message: string; }; export default function useCreateNonce() { const [createNonce] = usePromisifiedMutation( graphql` - mutation useCreateNonceMutation($chainAddress: ChainAddressInput!) { - getAuthNonce(chainAddress: $chainAddress) { + mutation useCreateNonceMutation { + getAuthNonce { __typename ... on AuthNonce { - userExists @required(action: THROW) nonce @required(action: THROW) - } - - ... on ErrDoesNotOwnRequiredToken { - message + message @required(action: THROW) } } } ` ); - return useCallback( - async (address: string, chain: Chain): Promise => { - // Kick off the mutation network request - // - // This call can throw an error. This error is the equivalent - // of either a 500, or a network error (the user didn't have connection) - // - // If this throws, we'll just let the UI handle that appropriately - // with it's try catch - const { getAuthNonce } = await createNonce({ - variables: { chainAddress: { address, chain } }, - }); - - // If the server didn't give us a payload for the mutation we just committed, - // we'll throw an error with a somewhat helpful message. This usually means - // the server panicked at some point in the stack and was unable to commit - // the mutation. - if (!getAuthNonce) { - throw new Error('getAuthNonce failed to execute. response data missing'); - } - - // The types generated by Relay let us do some great TypeScript - // union checks here. Inside of this if, we'll have narrowed the - // the type down to be the ErrDoesNotOwnRequiredToken type so we - // can access the relevant fields. - if (getAuthNonce?.__typename === 'ErrDoesNotOwnRequiredToken') { - const errorWithCode: Web3Error = { - name: getAuthNonce.__typename, - code: 'GALLERY_SERVER_ERROR', - message: getAuthNonce.message, - }; - - throw errorWithCode; - } - - // Same thing here. If the response's typename is AuthNonce - // that means the type has a nonce, and userExists field - if (getAuthNonce?.__typename === 'AuthNonce') { - return { nonce: getAuthNonce.nonce, user_exists: getAuthNonce.userExists }; - } - - // The server added some new type to the union and we don't know what to do. - throw new Error( - `Unexpected type returned from createNonceMutation: ${getAuthNonce.__typename}` - ); - }, - [createNonce] - ); + return useCallback(async (): Promise => { + // Kick off the mutation network request + // + // This call can throw an error. This error is the equivalent + // of either a 500, or a network error (the user didn't have connection) + // + // If this throws, we'll just let the UI handle that appropriately + // with it's try catch + const { getAuthNonce } = await createNonce({ variables: {} }); + + // If the server didn't give us a payload for the mutation we just committed, + // we'll throw an error with a somewhat helpful message. This usually means + // the server panicked at some point in the stack and was unable to commit + // the mutation. + if (!getAuthNonce) { + throw new Error('getAuthNonce failed to execute. response data missing'); + } + + // Same thing here. If the response's typename is AuthNonce + // that means the type has a nonce, and userExists field + if (getAuthNonce?.__typename === 'AuthNonce') { + return { nonce: getAuthNonce.nonce, message: getAuthNonce.message }; + } + + // The server added some new type to the union and we don't know what to do. + throw new Error( + `Unexpected type returned from createNonceMutation: ${getAuthNonce.__typename}` + ); + }, [createNonce]); } diff --git a/schema.graphql b/schema.graphql index 2ae43b4a9c..6b020f96b6 100644 --- a/schema.graphql +++ b/schema.graphql @@ -116,7 +116,7 @@ input AuthMechanism { type AuthNonce { nonce: String - userExists: Boolean + message: String } union AuthorizationError = ErrNoCookie | ErrInvalidToken | ErrSessionInvalidated | ErrDoesNotOwnRequiredToken @@ -588,9 +588,14 @@ type EnsProfileImage { input EoaAuth { chainPubKey: ChainPubKeyInput! nonce: String! + message: String! signature: String! } +type ErrAddressNotOwnedByUser implements Error { + message: String! +} + type ErrAddressOwnedByUser implements Error { message: String! } @@ -639,6 +644,18 @@ type ErrGalleryNotFound implements Error { message: String! } +type ErrHighlightChainNotSupported implements Error { + message: String! +} + +type ErrHighlightMintUnavailable implements Error { + message: String! +} + +type ErrHighlightTxnFailed implements Error { + message: String! +} + type ErrInvalidInput implements Error { message: String! parameters: [String!]! @@ -907,7 +924,7 @@ type GenerateQRCodeLoginTokenPayload { union GenerateQRCodeLoginTokenPayloadOrError = GenerateQRCodeLoginTokenPayload | ErrNotAuthorized -union GetAuthNoncePayloadOrError = AuthNonce | ErrDoesNotOwnRequiredToken +union GetAuthNoncePayload = AuthNonce type GIFMedia implements Media { previewURLs: PreviewURLSet @@ -931,6 +948,7 @@ type GltfMedia implements Media { input GnosisSafeAuth { address: Address! nonce: String! + message: String! } interface GroupedNotification { @@ -951,6 +969,30 @@ type GroupNotificationUsersConnection { pageInfo: PageInfo } +input HighlightClaimMintInput { + collectionId: String! + recipientWalletId: DBID! +} + +type HighlightClaimMintPayload { + claimId: DBID! +} + +union HighlightClaimMintPayloadOrError = HighlightClaimMintPayload | ErrHighlightTxnFailed | ErrHighlightMintUnavailable | ErrHighlightChainNotSupported | ErrAddressNotOwnedByUser | ErrNotAuthorized + +type HighlightMintClaimStatusPayload { + status: HighlightTxStatus! + token: Token +} + +union HighlightMintClaimStatusPayloadOrError = HighlightMintClaimStatusPayload | ErrHighlightTxnFailed | ErrHighlightMintUnavailable | ErrNotAuthorized + +enum HighlightTxStatus { + TX_PENDING + TX_COMPLETE + TOKEN_SYNCED +} + type HtmlMedia implements Media { previewURLs: PreviewURLSet mediaURL: String @@ -1166,7 +1208,7 @@ type Mutation { refreshToken(tokenId: DBID!): RefreshTokenPayloadOrError refreshCollection(collectionId: DBID!): RefreshCollectionPayloadOrError refreshContract(contractId: DBID!): RefreshContractPayloadOrError - getAuthNonce(chainAddress: ChainAddressInput!): GetAuthNoncePayloadOrError + getAuthNonce: GetAuthNoncePayload createUser(authMechanism: AuthMechanism!, input: CreateUserInput!): CreateUserPayloadOrError updateEmail(input: UpdateEmailInput!): UpdateEmailPayloadOrError resendVerificationEmail: ResendVerificationEmailPayloadOrError @@ -1199,6 +1241,7 @@ type Mutation { referralPostToken(input: ReferralPostTokenInput!): ReferralPostTokenPayloadOrError referralPostPreflight(input: ReferralPostPreflightInput!): ReferralPostPreflightPayloadOrError deletePost(postId: DBID!): DeletePostPayloadOrError + highlightClaimMint(input: HighlightClaimMintInput!): HighlightClaimMintPayloadOrError viewGallery(galleryId: DBID!): ViewGalleryPayloadOrError viewToken(tokenID: DBID!, collectionID: DBID!): ViewTokenPayloadOrError updateGallery(input: UpdateGalleryInput!): UpdateGalleryPayloadOrError @@ -1516,6 +1559,7 @@ type Query { postComposerDraftDetails(input: PostComposerDraftDetailsInput!): PostComposerDraftDetailsPayloadOrError contractCommunityByKey(key: ContractCommunityKeyInput!): CommunityByKeyOrError artBlocksCommunityByKey(key: ArtBlocksCommunityKeyInput!): CommunityByKeyOrError + highlightMintClaimStatus(claimId: DBID!): HighlightMintClaimStatusPayloadOrError } input RedeemMerchInput {