diff --git a/apps/web/src/components/ConnectWalletsFlow.vue b/apps/web/src/components/ConnectWalletsFlow.vue new file mode 100644 index 000000000..fc8b31479 --- /dev/null +++ b/apps/web/src/components/ConnectWalletsFlow.vue @@ -0,0 +1,623 @@ + + + + + \ No newline at end of file diff --git a/apps/web/src/components/LoaderSpinner.vue b/apps/web/src/components/LoaderSpinner.vue index 2abef0072..122f538d5 100644 --- a/apps/web/src/components/LoaderSpinner.vue +++ b/apps/web/src/components/LoaderSpinner.vue @@ -3,7 +3,18 @@ @@ -16,15 +27,17 @@ width: 100%; height: 100%; } + .loader_spinner div { transform-origin: 40px 20px; animation: loader_spinner 1.2s linear infinite; } + .loader_spinner div:after { content: " "; display: block; position: absolute; - top: 3px; + top: 30px; left: 37px; width: 4px; height: 10px; @@ -32,58 +45,72 @@ background: #0D5FFF; opacity: 0.4; } + .loader_spinner div:nth-child(1) { transform: rotate(0deg); animation-delay: -1.1s; } + .loader_spinner div:nth-child(2) { transform: rotate(30deg); animation-delay: -1s; } + .loader_spinner div:nth-child(3) { transform: rotate(60deg); animation-delay: -0.9s; } + .loader_spinner div:nth-child(4) { transform: rotate(90deg); animation-delay: -0.8s; } + .loader_spinner div:nth-child(5) { transform: rotate(120deg); animation-delay: -0.7s; } + .loader_spinner div:nth-child(6) { transform: rotate(150deg); animation-delay: -0.6s; } + .loader_spinner div:nth-child(7) { transform: rotate(180deg); animation-delay: -0.5s; } + .loader_spinner div:nth-child(8) { transform: rotate(210deg); animation-delay: -0.4s; } + .loader_spinner div:nth-child(9) { transform: rotate(240deg); animation-delay: -0.3s; } + .loader_spinner div:nth-child(10) { transform: rotate(270deg); animation-delay: -0.2s; } + .loader_spinner div:nth-child(11) { transform: rotate(300deg); animation-delay: -0.1s; } + .loader_spinner div:nth-child(12) { transform: rotate(330deg); animation-delay: 0s; } + @keyframes loader_spinner { 0% { opacity: 1; } + 100% { opacity: 0; } diff --git a/apps/web/src/composables/auth.ts b/apps/web/src/composables/auth.ts index 789cb1fe1..23d8aa609 100644 --- a/apps/web/src/composables/auth.ts +++ b/apps/web/src/composables/auth.ts @@ -1,89 +1,380 @@ +import * as Session from 'supertokens-web-js/recipe/session' +import { onMounted, onUnmounted, readonly, ref } from 'vue' import useEnvironment from '@/composables/environment' -import { SignInWithEthereumCredentials } from '@casimir/types' +import useEthers from '@/composables/ethers' +import useLedger from '@/composables/ledger' +import useTrezor from '@/composables/trezor' +import useUser from '@/composables/user' +import useWalletConnect from '@/composables/walletConnectV2' +import { + Account, + ApiResponse, + LoginCredentials, + ProviderString, + UserAuthState, +} from '@casimir/types' -const { domain, origin, usersUrl } = useEnvironment() +const { usersUrl } = useEnvironment() +const { ethersProviderList, detectActiveEthersWalletAddress, loginWithEthers } = + useEthers() +const { loginWithLedger } = useLedger() +const { loginWithTrezor } = useTrezor() +const { setUser, user } = useUser() +const { + loginWithWalletConnectV2, + initializeWalletConnect, + uninitializeWalletConnect, +} = useWalletConnect() + +const initializedAuthComposable = ref(false) +const loadingSessionLogin = ref(false) +const loadingSessionLoginError = ref(false) +const loadingSessionLogout = ref(false) +const loadingSessionLogoutError = ref(false) export default function useAuth() { + async function addAccountToUser({ + provider, + address, + currency, + }: { + provider: string; + address: string; + currency: string; + }) { + const userAccountExists = user.value?.accounts?.some( + (account: Account | any) => + account?.address === address && + account?.walletProvider === provider && + account?.currency === currency + ) + if (userAccountExists) return 'Account already exists for this user' + const account = { + userId: user?.value?.id, + address: address.toLowerCase() as string, + currency: currency || 'ETH', + ownerAddress: user?.value?.address.toLowerCase() as string, + walletProvider: provider, + } + + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ account, id: user?.value?.id }), + } + + try { + const response = await fetch( + `${usersUrl}/user/add-sub-account`, + requestOptions + ) + const { error, message, data: updatedUser } = await response.json() + if (error) + throw new Error(message || 'There was an error adding the account') + setUser(updatedUser) + return { error, message, data: updatedUser } + } catch (error: any) { + throw new Error(error.message || 'Error adding account') + } + } + + async function detectActiveWalletAddress(providerString: ProviderString) { + if (ethersProviderList.includes(providerString)) { + return await detectActiveEthersWalletAddress(providerString) + } else { + alert( + 'detectActiveWalletAddress not yet implemented for this wallet provider' + ) + } + } + + async function getUser() { + try { + const requestOptions = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + const response = await fetch(`${usersUrl}/user`, requestOptions) + const { user: retrievedUser, error } = await response.json() + if (error) throw new Error(error) + await setUser(retrievedUser) + } catch (error: any) { + throw new Error('Error getting user from API route') + } + } + + async function checkIfPrimaryUserExists( + provider: ProviderString, + address: string + ): Promise { + try { + const requestOptions = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + const response = await fetch( + `${usersUrl}/user/check-if-primary-address-exists/${address}`, + requestOptions + ) + const { error, message, data } = await response.json() + if (error) throw new Error(message) + return { error, message, data } + } catch (error: any) { + throw new Error(error.message || 'Error checking if primary user exists') + } + } + + async function login( + loginCredentials: LoginCredentials + ): Promise { + const { address, provider } = loginCredentials + try { + if (user.value) { + // If address already exists on user, do nothing + const addressExistsOnUser = user.value?.accounts?.some( + (account: Account | any) => account?.address === address + ) + if (addressExistsOnUser) return 'Address already exists on this account' + + // Check if it exists as a primary address of a different user + const { + data: { sameAddress, walletProvider }, + } = await checkIfPrimaryUserExists(provider as ProviderString, address) + // If yes, ask user if they want to add it as a secondary to this account or if they want to log in with that account + if (sameAddress) { + return 'Address already exists as a primary address on another account' + // If they want to add to account, addAccountToUser + // If they don't want to add to their account, cancel (or log them out and log in with other account) + } else { + // If no, check if it exists as a secondary address of a different user + const { data: accountsIfSecondaryAddress } = await checkIfSecondaryAddress(address) + // If yes, alert user that it already exists as a secondary address on another account and ask if they want to add it as a secondary to this account + if (accountsIfSecondaryAddress.length) { + console.log('accountsIfSecondaryAddress :>> ', accountsIfSecondaryAddress) + return 'Address already exists as a secondary address on another account' + } else { + // If no, addAccountToUser + await addAccountToUser(loginCredentials) + return 'Successfully added account to user' + } + } + } else { + // Check if address is a primary address and log in if so + const { data: { sameAddress, walletProvider } } = await checkIfPrimaryUserExists(provider as ProviderString, address) + if (sameAddress) { + await loginWithProvider(loginCredentials as LoginCredentials) + return 'Successfully logged in' + } - /** - * Creates the message from the server to sign, which includes getting the nonce from auth server - * - * @param {ProviderString} address - The address the user is using to sign in - * @param {string} statement - The statement the user is signing - * @returns {Promise} - The response from the message request - */ - async function createSiweMessage(address: string, statement: string) { - try { - const requestOptions = { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - address - }) - } - const res = await fetch(`${usersUrl}/auth/nonce`, requestOptions) - const { error, message: resMessage, data: nonce } = (await res.json()) - if (error) throw new Error(resMessage) - const message = { - domain, - address, - statement, - uri: origin, - version: '1', - chainId: 5, - nonce - } - return prepareMessage(message) - } catch (error: any) { - throw new Error(error.message || 'Error creating SIWE message') + // Then check if address is being used as a secondary account by another user + const { data: accountsIfSecondaryAddress } = + await checkIfSecondaryAddress(address) + console.log('accountsIfSecondaryAddress :>> ', accountsIfSecondaryAddress) + if (accountsIfSecondaryAddress.length) { + return 'Address already exists as a secondary address on another account' } + + // Handle user interaction (do they want to sign in with another account?) + // If yes, log out (and/or log them in with the other account) + // If no, cancel/do nothing + + const hardwareWallet = provider === 'Ledger' || provider === 'Trezor' + const browserWallet = ethersProviderList.includes( + provider as ProviderString + ) + if (hardwareWallet) { + await loginWithProvider(loginCredentials as LoginCredentials) + await getUser() + return 'Successfully logged in' + } else if (browserWallet) { + const activeAddress = await detectActiveWalletAddress( + provider as ProviderString + ) + if (activeAddress === address) { + await loginWithProvider({ + provider: provider as ProviderString, + address, + currency: 'ETH', + }) + return 'Successfully logged in' + } else { + return 'Selected address is not active address in wallet' + } + } else { + return 'Error in userAuthState' + } + } + } catch (error: any) { + return 'Error in userAuthState' } + } - /** - * Signs user up if they don't exist, otherwise - * logs the user in with an address, message, and signed message - * - * @param {SignInWithEthereumCredentials} signInWithEthereumCredentials - The user's address, provider, currency, message, and signed message - * @returns {Promise} - The response from the login request - */ - async function signInWithEthereum(signInWithEthereumCredentials: SignInWithEthereumCredentials): Promise { - try { - const requestOptions = { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(signInWithEthereumCredentials) - } - const response = await fetch(`${usersUrl}/auth/login`, requestOptions) - const { error, message } = await response.json() - if (error) throw new Error(message) - } catch (error: any) { - throw new Error(error.message || 'Error signing in with Ethereum') + async function loginWithSecondaryAddress(loginCredentials: LoginCredentials) { + const { address, provider } = loginCredentials + try { + const hardwareWallet = provider === 'Ledger' || provider === 'Trezor' + const browserWallet = ethersProviderList.includes(provider as ProviderString) + if (hardwareWallet) { + await loginWithProvider(loginCredentials as LoginCredentials) + await getUser() + return 'Successfully created account and logged in' + } else if (browserWallet) { + const activeAddress = await detectActiveWalletAddress(provider as ProviderString) + if (activeAddress === address) { + await loginWithProvider({ provider: provider as ProviderString, address,currency: 'ETH' }) + return 'Successfully created account and logged in' + } else { + return 'Selected address is not active address in wallet' } + } + } catch(err) { + return 'Error in userAuthState' } + } - return { - createSiweMessage, - signInWithEthereum + async function checkIfSecondaryAddress( + address: string + ): Promise { + try { + const requestOptions = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + const response = await fetch( + `${usersUrl}/user/check-secondary-address/${address}`, + requestOptions + ) + const { error, message, data } = await response.json() + if (error) throw new Error(message) + return { error, message, data } + } catch (error: any) { + throw new Error(error.message || 'Error checking if secondary address') } -} + } + + /** + * Uses appropriate provider composable to login or sign up + * @param provider + * @param address + * @param currency + * @returns + */ + async function loginWithProvider(loginCredentials: LoginCredentials) { + const { provider } = loginCredentials + try { + if (ethersProviderList.includes(provider)) { + await loginWithEthers(loginCredentials) + } else if (provider === 'Ledger') { + await loginWithLedger(loginCredentials) + } else if (provider === 'Trezor') { + await loginWithTrezor(loginCredentials) + } else if (provider === 'WalletConnect') { + await loginWithWalletConnectV2(loginCredentials) + } else { + console.log('Sign up not yet supported for this wallet provider') + } + await getUser() + } catch (error: any) { + throw new Error(error.message || 'There was an error logging in') + } + } + + async function logout() { + // Loader + try { + loadingSessionLogout.value = true + await Session.signOut() + await setUser(undefined) + loadingSessionLogout.value = false + } catch (error) { + loadingSessionLogoutError.value = true + console.log('Error logging out user :>> ', error) + loadingSessionLogout.value = false + } + // TODO: Fix bug that doesn't allow you to log in without refreshing page after a user logs out + window.location.reload() + } -function prepareMessage(obj: any) { - const { - domain, - address, - statement, - uri, - version, - chainId, - nonce, - } = obj - - const issuedAt = new Date().toISOString() - const message = `${domain} wants you to sign in with your Ethereum account:\n${address}\n\n${statement}\n\nURI: ${uri}\nVersion: ${version}\nChain ID: ${chainId}\nNonce: ${nonce}\nIssued At: ${issuedAt}` - - return message -} \ No newline at end of file + onMounted(async () => { + if (!initializedAuthComposable.value) { + initializedAuthComposable.value = true + // Loader + try { + loadingSessionLogin.value = true + const session = await Session.doesSessionExist() + if (session) await getUser() + await initializeWalletConnect() // Can potentially move this elsewhere + loadingSessionLogin.value = false + } catch (error) { + loadingSessionLoginError.value = true + console.log('error getting user :>> ', error) + loadingSessionLogin.value = false + } + } + }) + + onUnmounted(() => { + initializedAuthComposable.value = false + uninitializeWalletConnect() + }) + + // TODO: Re-enable once we have a way to remove accounts in UI + // async function removeConnectedAccount() { + // if (!user?.value?.address) { + // alert('Please login first') + // } + // if (selectedAddress.value === primaryAddress.value) { + // return alert('Cannot remove primary account') + // } else if (ethersProviderList.includes(selectedProvider.value)) { + // const opts = { + // address: selectedAddress.value, + // currency: selectedCurrency.value, + // ownerAddress: primaryAddress.value, + // walletProvider: selectedProvider.value + // } + // const removeAccountResult = await removeAccount(opts) + // if (!removeAccountResult.error) { + // setSelectedAddress(removeAccountResult.data.address) + // removeAccountResult.data.accounts.forEach((account: Account) => { + // if (account.address === selectedAddress.value) { + // setSelectedProvider(account.walletProvider as ProviderString) + // setSelectedCurrency(account.currency as Currency) + // } + // }) + // } + // } + // } + + async function updatePrimaryAddress(updatedAddress: string) { + const userId = user?.value?.id + const requestOptions = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ userId, updatedAddress }), + } + return await fetch( + `${usersUrl}/user/update-primary-account`, + requestOptions + ) + } + + return { + loadingSessionLogin: readonly(loadingSessionLogin), + loadingSessionLoginError: readonly(loadingSessionLoginError), + loadingSessionLogout: readonly(loadingSessionLogout), + loadingSessionLogoutError: readonly(loadingSessionLogoutError), + login, + loginWithSecondaryAddress, + logout, + } +} diff --git a/apps/web/src/composables/breakdownMetrics.ts b/apps/web/src/composables/breakdownMetrics.ts index ce0b9b8da..0aa5adecb 100644 --- a/apps/web/src/composables/breakdownMetrics.ts +++ b/apps/web/src/composables/breakdownMetrics.ts @@ -1,18 +1,14 @@ import { readonly, ref, toValue } from 'vue' import { ethers } from 'ethers' import { Account, BreakdownAmount, BreakdownString, ContractEventsByAddress, UserWithAccountsAndOperators } from '@casimir/types' -import useContracts from '@/composables/contracts' import useEnvironment from '@/composables/environment' import useFormat from '@/composables/format' import usePrice from '@/composables/price' -const { manager } = useContracts() -const { ethereumUrl } = useEnvironment() +const { manager, provider } = useEnvironment() const { formatNumber } = useFormat() const { getCurrentPrice } = usePrice() -const provider = new ethers.providers.JsonRpcProvider(ethereumUrl) - const loadingInitializeBreakdownMetrics = ref(false) const loadingInitializeBreakdownMetricsError = ref(false) diff --git a/apps/web/src/composables/environment.ts b/apps/web/src/composables/environment.ts index 0b4791602..f55e36cbc 100644 --- a/apps/web/src/composables/environment.ts +++ b/apps/web/src/composables/environment.ts @@ -1,33 +1,49 @@ +import { ethers } from 'ethers' +import { CasimirManager, CasimirRegistry, CasimirViews } from '@casimir/ethereum/build/@types' +import CasimirManagerAbi from '@casimir/ethereum/build/abi/CasimirManager.json' +import CasimirRegistryAbi from '@casimir/ethereum/build/abi/CasimirRegistry.json' +import CasimirViewsAbi from '@casimir/ethereum/build/abi/CasimirViews.json' + +const domain = window.location.host +const origin = window.location.origin +const usersUrl = import.meta.env.PUBLIC_USERS_URL || 'http://localhost:4000' +const ethereumUrl = import.meta.env.PUBLIC_ETHEREUM_RPC_URL || 'http://127.0.0.1:8545' +const ledgerType = import.meta.env.PUBLIC_SPECULOS_URL ? 'speculos' : 'usb' +const speculosUrl = import.meta.env.PUBLIC_SPECULOS_URL ? 'http://localhost:5001' : '' +const cryptoCompareApiKey = import.meta.env.PUBLIC_CRYPTO_COMPARE_API_KEY || '' +const ssvNetworkAddress = import.meta.env.PUBLIC_SSV_NETWORK_ADDRESS +const ssvViewsAddress = import.meta.env.PUBLIC_SSV_VIEWS_ADDRESS +const walletConnectProjectId = import.meta.env.PUBLIC_WALLET_CONNECT_PROJECT_ID + +/* Contracts */ +const managerAddress = import.meta.env.PUBLIC_MANAGER_ADDRESS +if (!managerAddress) throw new Error('No manager address provided') +const viewsAddress = import.meta.env.PUBLIC_VIEWS_ADDRESS +if (!viewsAddress) throw new Error('No views address provided') +const registryAddress = import.meta.env.PUBLIC_REGISTRY_ADDRESS +if (!registryAddress) throw new Error('No registry address provided') +const provider = new ethers.providers.JsonRpcProvider(ethereumUrl) +const manager: CasimirManager & ethers.Contract = new ethers.Contract(managerAddress, CasimirManagerAbi, provider) as CasimirManager +const views: CasimirViews & ethers.Contract = new ethers.Contract(viewsAddress, CasimirViewsAbi, provider) as CasimirViews +const registry: CasimirRegistry & ethers.Contract = new ethers.Contract(registryAddress, CasimirRegistryAbi, provider) as CasimirRegistry + export default function useEnvironment() { - const domain = window.location.host - const origin = window.location.origin - const managerAddress = import.meta.env.PUBLIC_MANAGER_ADDRESS - if (!managerAddress) throw new Error('No manager address provided') - const viewsAddress = import.meta.env.PUBLIC_VIEWS_ADDRESS - if (!viewsAddress) throw new Error('No views address provided') - const registryAddress = import.meta.env.PUBLIC_REGISTRY_ADDRESS - if (!registryAddress) throw new Error('No registry address provided') - const usersUrl = import.meta.env.PUBLIC_USERS_URL || 'http://localhost:4000' - const ethereumUrl = import.meta.env.PUBLIC_ETHEREUM_RPC_URL || 'http://127.0.0.1:8545' - const ledgerType = import.meta.env.PUBLIC_SPECULOS_URL ? 'speculos' : 'usb' - const speculosUrl = import.meta.env.PUBLIC_SPECULOS_URL ? 'http://localhost:5001' : '' - const cryptoCompareApiKey = import.meta.env.PUBLIC_CRYPTO_COMPARE_API_KEY || '' - const ssvNetworkAddress = import.meta.env.PUBLIC_SSV_NETWORK_ADDRESS - const ssvViewsAddress = import.meta.env.PUBLIC_SSV_VIEWS_ADDRESS - const walletConnectProjectId = import.meta.env.PUBLIC_WALLET_CONNECT_PROJECT_ID - return { domain, cryptoCompareApiKey, ethereumUrl, + manager, + provider, origin, ledgerType, managerAddress, registryAddress, + registry, speculosUrl, ssvNetworkAddress, ssvViewsAddress, usersUrl, + views, viewsAddress, walletConnectProjectId } diff --git a/apps/web/src/composables/ethers.ts b/apps/web/src/composables/ethers.ts index b1ba2b07a..7766ae55a 100644 --- a/apps/web/src/composables/ethers.ts +++ b/apps/web/src/composables/ethers.ts @@ -1,18 +1,17 @@ import { ethers } from 'ethers' -import { EthersProvider } from '@casimir/types' -import { Account, TransactionRequest, UserWithAccountsAndOperators } from '@casimir/types' +import { CryptoAddress, EthersProvider } from '@casimir/types' +import { TransactionRequest } from '@casimir/types' import { GasEstimate, LoginCredentials, MessageRequest, ProviderString } from '@casimir/types' -import useAuth from '@/composables/auth' import useEnvironment from '@/composables/environment' +import useSiwe from '@/composables/siwe' interface ethereumWindow extends Window { ethereum: any; } declare const window: ethereumWindow -const { createSiweMessage, signInWithEthereum } = useAuth() -const { ethereumUrl } = useEnvironment() -const provider = new ethers.providers.JsonRpcProvider(ethereumUrl) +const { ethereumUrl, provider } = useEnvironment() +const { createSiweMessage, signInWithEthereum } = useSiwe() export default function useEthers() { const ethersProviderList = ['BraveWallet', 'CoinbaseWallet', 'MetaMask', 'OkxWallet', 'TrustWallet'] @@ -29,6 +28,24 @@ export default function useEthers() { } } + async function detectActiveEthersWalletAddress(providerString: ProviderString): Promise { + const provider = getBrowserProvider(providerString) + try { + if (provider) { + const accounts = await provider.request({ method: 'eth_accounts' }) + if (accounts.length > 0) { + return accounts[0] as string + } + return '' + } else { + return '' + } + } catch(err) { + console.error('There was an error in detectActiveEthersWalletAddress :>> ', err) + return '' + } + } + /** * Estimate gas fee using EIP 1559 methodology * @returns string in ETH @@ -98,13 +115,17 @@ export default function useEthers() { } } - async function getEthersAddressWithBalance (providerString: ProviderString) { + async function getEthersAddressesWithBalances (providerString: ProviderString): Promise { const provider = getBrowserProvider(providerString) - if (provider) { - const address = (await requestEthersAccount(provider as EthersProvider))[0] - const balance = (await getEthersBalance(address)).toString() - return [{ address, balance }] + const addresses = await provider.request({ method: 'eth_requestAccounts' }) + const addressesWithBalance: CryptoAddress[] = [] + for (const address of addresses) { + const balance = (await getEthersBalance(address)).toString() + const addressWithBalance = { address, balance } + addressesWithBalance.push(addressWithBalance) + } + return addressesWithBalance } else { throw new Error('Provider not yet connected to this dapp. Please connect and try again.') } @@ -169,14 +190,6 @@ export default function useEthers() { } } - async function requestEthersAccount(provider: EthersProvider) { - if (provider?.request) { - return await provider.request({ - method: 'eth_requestAccounts', - }) - } - } - async function sendEthersTransaction( { from, to, value, providerString }: TransactionRequest ) { @@ -235,37 +248,40 @@ export default function useEthers() { } return { - addEthersNetwork, - estimateEIP1559GasFee, - estimateLegacyGasFee, ethersProviderList, - getEthersAddressWithBalance, + detectActiveEthersWalletAddress, + getEthersAddressesWithBalances, getEthersBalance, - getEthersBrowserProviderSelectedCurrency, getEthersBrowserSigner, getGasPriceAndLimit, - getMaxETHAfterFees, loginWithEthers, - requestEthersAccount, - sendEthersTransaction, - signEthersMessage, - switchEthersNetwork } } function getBrowserProvider(providerString: ProviderString) { - if (providerString === 'MetaMask' || providerString === 'CoinbaseWallet') { + try { const { ethereum } = window - if (!ethereum.providerMap && ethereum.isMetaMask) { - return ethereum + if (providerString === 'CoinbaseWallet') { + if (!ethereum.providerMap) return alert('TrustWallet or another wallet may be interfering with CoinbaseWallet. Please disable other wallets and try again.') + if (ethereum?.providerMap.get(providerString)) return ethereum.providerMap.get(providerString) + else window.open('https://www.coinbase.com/wallet/downloads', '_blank') + } else if (providerString === 'MetaMask') { + if (ethereum.providerMap && ethereum.providerMap.get('MetaMask')) { + return ethereum?.providerMap?.get(providerString) || undefined + } else if (ethereum.isMetaMask) { + return ethereum + } else { + window.open('https://metamask.io/download.html', '_blank') + } + } else if (providerString === 'BraveWallet') { + return getBraveWallet() + } else if (providerString === 'TrustWallet') { + return getTrustWallet() + } else if (providerString === 'OkxWallet') { + return getOkxWallet() } - return ethereum?.providerMap?.get(providerString) || undefined - } else if (providerString === 'BraveWallet') { - return getBraveWallet() - } else if (providerString === 'TrustWallet') { - return getTrustWallet() - } else if (providerString === 'OkxWallet') { - return getOkxWallet() + } catch(err) { + console.error('There was an error in getBrowserProvider :>> ', err) } } @@ -287,6 +303,7 @@ function getOkxWallet() { function getTrustWallet() { const { ethereum } = window as any const providers = ethereum?.providers + if (ethereum.isTrust) return ethereum if (providers) { for (const provider of providers) { if (provider.isTrustWallet) return provider diff --git a/apps/web/src/composables/ledger.ts b/apps/web/src/composables/ledger.ts index c2646381b..1626d8f6e 100644 --- a/apps/web/src/composables/ledger.ts +++ b/apps/web/src/composables/ledger.ts @@ -4,9 +4,9 @@ import { MessageRequest, TransactionRequest } from '@casimir/types' import { CryptoAddress, LoginCredentials } from '@casimir/types' import useEnvironment from '@/composables/environment' import useEthers from '@/composables/ethers' -import useAuth from '@/composables/auth' +import useSiwe from '@/composables/siwe' -const { createSiweMessage, signInWithEthereum } = useAuth() +const { createSiweMessage, signInWithEthereum } = useSiwe() export default function useLedger() { const { ethereumUrl, ledgerType, speculosUrl } = useEnvironment() @@ -20,11 +20,13 @@ export default function useLedger() { // return new BitcoinLedgerSigner(options) // } - function getEthersLedgerSigner() { + function getEthersLedgerSigner(pathIndex?: number) { + const path = pathIndex ? `m/44'/60'/0'/0/${pathIndex}` : 'm/44\'/60\'/0\'/0/0' const options = { provider: new ethers.providers.JsonRpcProvider(ethereumUrl), type: ledgerType, - baseURL: speculosUrl + baseURL: speculosUrl, + path } return new EthersLedgerSigner(options) } @@ -68,16 +70,16 @@ export default function useLedger() { return await signer.getAddresses() as Array } - async function loginWithLedger(loginCredentials: LoginCredentials, pathIndex: string) { + async function loginWithLedger(loginCredentials: LoginCredentials) { // ETH Mainnet: 0x8222ef172a2117d1c4739e35234e097630d94376 // ETH Goerli 1: 0x8222Ef172A2117D1C4739E35234E097630D94376 // ETH Goerli 2: 0x8ed535c94DC22218D74A77593228cbb1B7FF6D13 // Derivation path m/44\'/60\'/0\'/0/1: 0x1a16ae0F5cf84CaE346a1D586d00366bBA69bccc - const { provider, address, currency } = loginCredentials + const { provider, address, currency, pathIndex } = loginCredentials try { const message = await createSiweMessage(address, 'Sign in with Ethereum to the app.') - const signer = getEthersLedgerSigner() - const signedMessage = await signer.signMessageWithIndex(message, pathIndex) + const signer = getEthersLedgerSigner(pathIndex) + const signedMessage = await signer.signMessageWithIndex(message, pathIndex as number) await signInWithEthereum({ address, currency, diff --git a/apps/web/src/composables/operators.ts b/apps/web/src/composables/operators.ts index 2b39dd992..cee14871f 100644 --- a/apps/web/src/composables/operators.ts +++ b/apps/web/src/composables/operators.ts @@ -1,15 +1,13 @@ import { readonly, ref } from 'vue' -import useEnvironment from '@/composables/environment' -import useContracts from '@/composables/contracts' import { Operator, Scanner } from '@casimir/ssv' import { Account, Pool, RegisteredOperator, RegisterOperatorWithCasimirParams, UserWithAccountsAndOperators } from '@casimir/types' import { ethers } from 'ethers' +import useEnvironment from '@/composables/environment' import useEthers from '@/composables/ethers' import useLedger from '@/composables/ledger' import useTrezor from '@/composables/trezor' -const { manager, registry, views } = useContracts() -const { ethereumUrl, ssvNetworkAddress, ssvViewsAddress, usersUrl } = useEnvironment() +const { ethereumUrl, manager, registry, ssvNetworkAddress, ssvViewsAddress, usersUrl, views } = useEnvironment() const { ethersProviderList, getEthersBrowserSigner } = useEthers() const { getEthersLedgerSigner } = useLedger() const { getEthersTrezorSigner } = useTrezor() @@ -121,16 +119,6 @@ export default function useOperators() { return pools } - function listenForContractEvents(user: UserWithAccountsAndOperators) { - try { - registry.on('OperatorRegistered', () => getUserOperators(user)) - // registry.on('OperatorDeregistered', getUserOperators) - // registry.on('DeregistrationRequested', getUserOperators) - } catch (err) { - console.log(`There was an error in listenForContractEvents: ${err}`) - } - } - async function initializeComposable(user: UserWithAccountsAndOperators){ try { loadingInitializeOperators.value = true @@ -144,6 +132,16 @@ export default function useOperators() { } } + function listenForContractEvents(user: UserWithAccountsAndOperators) { + try { + registry.on('OperatorRegistered', () => getUserOperators(user)) + // registry.on('OperatorDeregistered', getUserOperators) + // registry.on('DeregistrationRequested', getUserOperators) + } catch (err) { + console.log(`There was an error in listenForContractEvents: ${err}`) + } + } + // TODO: Move this to operators.ts to combine with AddOperator method async function registerOperatorWithCasimir({ walletProvider, address, operatorId, collateral, nodeUrl }: RegisterOperatorWithCasimirParams) { loadingRegisteredOperators.value = true @@ -183,4 +181,46 @@ export default function useOperators() { initializeComposable, registerOperatorWithCasimir, } -} \ No newline at end of file +} + +// async function _getSSVOperators(): Promise { +// const ownerAddresses = (user?.value as UserWithAccountsAndOperators).accounts.map((account: Account) => account.address) as string[] +// // const ownerAddressesTest = ['0x9725Dc287005CB8F11CA628Bb769E4A4Fc8f0309'] +// try { +// // const promises = ownerAddressesTest.map((address) => _querySSVOperators(address)) +// const promises = ownerAddresses.map((address) => _querySSVOperators(address)) +// const settledPromises = await Promise.allSettled(promises) as Array> +// const operators = settledPromises +// .filter((result) => result.status === 'fulfilled') +// .map((result) => result.value) + +// const ssvOperators = (operators[0] as Array).map((operator) => { +// const { id, fee, name, owner_address, performance } = operator +// return { +// id: id.toString(), +// fee: ethers.utils.formatEther(fee), +// name, +// ownerAddress: owner_address, +// performance +// } as SSVOperator +// }) + +// return ssvOperators +// } catch (err) { +// console.error(`There was an error in _getSSVOperators function: ${JSON.stringify(err)}`) +// return [] +// } +// } + +// async function _querySSVOperators(address: string) { +// try { +// const network = 'prater' +// const url = `https://api.ssv.network/api/v4/${network}/operators/owned_by/${address}` +// const response = await fetch(url) +// const { operators } = await response.json() +// return operators +// } catch (err) { +// console.error(`There was an error in _querySSVOperators function: ${JSON.stringify(err)}`) +// return [] +// } +// } \ No newline at end of file diff --git a/apps/web/src/composables/siwe.ts b/apps/web/src/composables/siwe.ts new file mode 100644 index 000000000..aa58e281e --- /dev/null +++ b/apps/web/src/composables/siwe.ts @@ -0,0 +1,88 @@ +import useEnvironment from '@/composables/environment' +import { SignInWithEthereumCredentials } from '@casimir/types' + +const { domain, usersUrl } = useEnvironment() + +export default function useSiwe() { + /** + * Creates the message from the server to sign, which includes getting the nonce from auth server + * + * @param {ProviderString} address - The address the user is using to sign in + * @param {string} statement - The statement the user is signing + * @returns {Promise} - The response from the message request + */ + async function createSiweMessage(address: string, statement: string) { + try { + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + address + }) + } + const res = await fetch(`${usersUrl}/auth/nonce`, requestOptions) + const { error, message: resMessage, data: nonce } = (await res.json()) + if (error) throw new Error(resMessage) + const message = { + domain, + address, + statement, + uri: origin, + version: '1', + chainId: 5, + nonce + } + return prepareMessage(message) + } catch (error: any) { + throw new Error(error.message || 'Error creating SIWE message') + } + } + + /** + * Signs user up if they don't exist, otherwise + * logs the user in with an address, message, and signed message + * + * @param {SignInWithEthereumCredentials} signInWithEthereumCredentials - The user's address, provider, currency, message, and signed message + * @returns {Promise} - The response from the login request + */ + async function signInWithEthereum(signInWithEthereumCredentials: SignInWithEthereumCredentials): Promise { + try { + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(signInWithEthereumCredentials) + } + const response = await fetch(`${usersUrl}/auth/login`, requestOptions) + const { error, message } = await response.json() + if (error) throw new Error(message) + } catch (error: any) { + throw new Error(error.message || 'Error signing in with Ethereum') + } + } + + return { + createSiweMessage, + signInWithEthereum + } +} + +function prepareMessage(obj: any) { + const { + domain, + address, + statement, + uri, + version, + chainId, + nonce, + } = obj + + const issuedAt = new Date().toISOString() + const message = `${domain} wants you to sign in with your Ethereum account:\n${address}\n\n${statement}\n\nURI: ${uri}\nVersion: ${version}\nChain ID: ${chainId}\nNonce: ${nonce}\nIssued At: ${issuedAt}` + + return message +} \ No newline at end of file diff --git a/apps/web/src/composables/contracts.ts b/apps/web/src/composables/staking.ts similarity index 54% rename from apps/web/src/composables/contracts.ts rename to apps/web/src/composables/staking.ts index fb06259ea..cf33c4ca4 100644 --- a/apps/web/src/composables/contracts.ts +++ b/apps/web/src/composables/staking.ts @@ -1,30 +1,18 @@ -import { ref, readonly } from 'vue' import { ethers } from 'ethers' -import { CasimirManager, CasimirRegistry, CasimirViews } from '@casimir/ethereum/build/@types' -import CasimirManagerAbi from '@casimir/ethereum/build/abi/CasimirManager.json' -import CasimirRegistryAbi from '@casimir/ethereum/build/abi/CasimirRegistry.json' -import CasimirViewsAbi from '@casimir/ethereum/build/abi/CasimirViews.json' +import { ProviderString } from '@casimir/types' import useEnvironment from './environment' import useEthers from '@/composables/ethers' import useLedger from '@/composables/ledger' import useTrezor from '@/composables/trezor' import useWalletConnectV2 from './walletConnectV2' -import { ProviderString } from '@casimir/types' -import { Operator } from '@casimir/ssv' - -const { ethereumUrl, managerAddress, registryAddress, viewsAddress } = useEnvironment() -const provider = new ethers.providers.JsonRpcProvider(ethereumUrl) -const manager: CasimirManager & ethers.Contract = new ethers.Contract(managerAddress, CasimirManagerAbi, provider) as CasimirManager -const views: CasimirViews & ethers.Contract = new ethers.Contract(viewsAddress, CasimirViewsAbi, provider) as CasimirViews -const registry: CasimirRegistry & ethers.Contract = new ethers.Contract(registryAddress, CasimirRegistryAbi, provider) as CasimirRegistry -const operators = ref([]) +const { manager } = useEnvironment() const { ethersProviderList, getEthersBrowserSigner } = useEthers() const { getEthersLedgerSigner } = useLedger() const { getEthersTrezorSigner } = useTrezor() const { getWalletConnectSignerV2 } = useWalletConnectV2() -export default function useContracts() { +export default function useStaking() { async function deposit({ amount, walletProvider }: { amount: string, walletProvider: ProviderString }) { try { @@ -55,43 +43,16 @@ export default function useContracts() { async function getDepositFees(): Promise { try { - const fees = await manager.FEE_PERCENT() + // TODO: Fix this bug + // const fees = await manager.FEE_PERCENT() + const fees = 5 const feesRounded = Math.round(fees * 100) / 100 return feesRounded } catch (err: any) { console.error(`There was an error in getDepositFees function: ${JSON.stringify(err)}`) throw new Error(err) } - } - - // async function _getSSVOperators(): Promise { - // const ownerAddresses = (user?.value as UserWithAccountsAndOperators).accounts.map((account: Account) => account.address) as string[] - // // const ownerAddressesTest = ['0x9725Dc287005CB8F11CA628Bb769E4A4Fc8f0309'] - // try { - // // const promises = ownerAddressesTest.map((address) => _querySSVOperators(address)) - // const promises = ownerAddresses.map((address) => _querySSVOperators(address)) - // const settledPromises = await Promise.allSettled(promises) as Array> - // const operators = settledPromises - // .filter((result) => result.status === 'fulfilled') - // .map((result) => result.value) - - // const ssvOperators = (operators[0] as Array).map((operator) => { - // const { id, fee, name, owner_address, performance } = operator - // return { - // id: id.toString(), - // fee: ethers.utils.formatEther(fee), - // name, - // ownerAddress: owner_address, - // performance - // } as SSVOperator - // }) - - // return ssvOperators - // } catch (err) { - // console.error(`There was an error in _getSSVOperators function: ${JSON.stringify(err)}`) - // return [] - // } - // } + } async function getUserStake(address: string): Promise { try { @@ -104,19 +65,6 @@ export default function useContracts() { } } - async function _querySSVOperators(address: string) { - try { - const network = 'prater' - const url = `https://api.ssv.network/api/v4/${network}/operators/owned_by/${address}` - const response = await fetch(url) - const { operators } = await response.json() - return operators - } catch (err) { - console.error(`There was an error in _querySSVOperators function: ${JSON.stringify(err)}`) - return [] - } - } - async function withdraw({ amount, walletProvider }: { amount: string, walletProvider: ProviderString }) { const signerCreators = { 'Browser': getEthersBrowserSigner, @@ -140,10 +88,6 @@ export default function useContracts() { } return { - manager, - operators, - registry, - views, deposit, getDepositFees, getUserStake, diff --git a/apps/web/src/composables/trezor.ts b/apps/web/src/composables/trezor.ts index 25d320a34..f5b5de085 100644 --- a/apps/web/src/composables/trezor.ts +++ b/apps/web/src/composables/trezor.ts @@ -1,11 +1,11 @@ import { EthersTrezorSigner } from '@casimir/wallets' -import useAuth from '@/composables/auth' +import useSiwe from '@/composables/siwe' import useEthers from '@/composables/ethers' import useEnvironment from '@/composables/environment' import { ethers } from 'ethers' import { LoginCredentials, MessageRequest, TransactionRequest } from '@casimir/types' -const { createSiweMessage, signInWithEthereum } = useAuth() +const { createSiweMessage, signInWithEthereum } = useSiwe() const trezorPath = 'm/44\'/60\'/0\'/0/0' @@ -69,12 +69,12 @@ export default function useTrezor() { return await signer.getAddresses() } - async function loginWithTrezor(loginCredentials: LoginCredentials, pathIndex: string) { - const { provider, address, currency } = loginCredentials + async function loginWithTrezor(loginCredentials: LoginCredentials) { + const { provider, address, currency, pathIndex } = loginCredentials try { const message = await createSiweMessage(address, 'Sign in with Ethereum to the app.') const signer = getEthersTrezorSigner() - const signedMessage = await signer.signMessageWithIndex(message, pathIndex) + const signedMessage = await signer.signMessageWithIndex(message, pathIndex as number) await signInWithEthereum({ address, currency, @@ -119,5 +119,11 @@ export default function useTrezor() { return await signer.signMessage(message) } - return { getEthersTrezorSigner, getTrezorAddress, loginWithTrezor, sendTrezorTransaction, signTrezorMessage } + return { + getEthersTrezorSigner, + getTrezorAddress, + loginWithTrezor, + sendTrezorTransaction, + signTrezorMessage + } } \ No newline at end of file diff --git a/apps/web/src/composables/user.ts b/apps/web/src/composables/user.ts index 9731496fb..1700493da 100644 --- a/apps/web/src/composables/user.ts +++ b/apps/web/src/composables/user.ts @@ -1,226 +1,80 @@ -import { onMounted, onUnmounted, readonly, ref } from 'vue' -import * as Session from 'supertokens-web-js/recipe/session' -import { ethers } from 'ethers' -import { Account, LoginCredentials, UserWithAccountsAndOperators } from '@casimir/types' +import { onMounted, onUnmounted, readonly, ref, watch } from 'vue' +import { Account, UserWithAccountsAndOperators } from '@casimir/types' import useEnvironment from '@/composables/environment' -import useEthers from '@/composables/ethers' -import useLedger from '@/composables/ledger' -import useTrezor from '@/composables/trezor' -import useWalletConnect from '@/composables/walletConnectV2' +import { ethers } from 'ethers' // Test address: 0xd557a5745d4560B24D36A68b52351ffF9c86A212 -const { ethereumUrl, usersUrl } = useEnvironment() -const { ethersProviderList, loginWithEthers } = useEthers() -const { loginWithLedger } = useLedger() -const { loginWithTrezor } = useTrezor() -const { loginWithWalletConnectV2, initializeWalletConnect, uninitializeWalletConnect } = useWalletConnect() +const { provider, usersUrl } = useEnvironment() const initializeComposable = ref(false) -const provider = new ethers.providers.JsonRpcProvider(ethereumUrl) const user = ref(undefined) -const loadingSessionLogin = ref(false) -const loadingSessionLoginError = ref(false) -const loadingSessionLogout = ref(false) -const loadingSessionLogoutError = ref(false) export default function useUser() { - async function addAccountToUser({ provider, address, currency }: { provider: string, address: string, currency: string}) { - const userAccountExists = user.value?.accounts?.some((account: Account | any) => account?.address === address && account?.walletProvider === provider && account?.currency === currency) - if (userAccountExists) return 'Account already exists for this user' - const account = { - userId: user?.value?.id, - address: address.toLowerCase() as string, - currency: currency || 'ETH', - ownerAddress: user?.value?.address.toLowerCase() as string, - walletProvider: provider - } + // TODO: Move back to ethers composable + async function getEthersBalance(address: string): Promise { + const balance = await provider.getBalance(address) + return parseFloat(ethers.utils.formatEther(balance)) + } - const requestOptions = { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ account, id: user?.value?.id }) - } + // onMounted(async () => { + // if (!initializeComposable.value) { + // initializeComposable.value = true + // } + // }) - try { - const response = await fetch(`${usersUrl}/user/add-sub-account`, requestOptions) - const { error, message, data: updatedUser } = await response.json() - if (error) throw new Error(message || 'There was an error adding the account') - user.value = updatedUser - await setUserAccountBalances() - return { error, message, data: updatedUser } - } catch (error: any) { - throw new Error(error.message || 'Error adding account') - } - } + // onUnmounted(() => { + // initializeComposable.value = false + // }) - async function getUser() { - try { - const requestOptions = { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - } - const response = await fetch(`${usersUrl}/user`, requestOptions) - const { user: retrievedUser, error } = await response.json() - if (error) throw new Error(error) - user.value = retrievedUser - } catch (error: any) { - throw new Error('Error getting user from API route') - } - } + async function setUser( + newUserValue: UserWithAccountsAndOperators | undefined + ) { + user.value = newUserValue + } - /** - * Uses appropriate provider composable to login or sign up - * @param provider - * @param address - * @param currency - * @returns - */ - async function login(loginCredentials: LoginCredentials, pathIndex?: number) { - const { provider } = loginCredentials - try { - if (ethersProviderList.includes(provider)) { - await loginWithEthers(loginCredentials) - } else if (provider === 'Ledger') { - await loginWithLedger(loginCredentials, JSON.stringify(pathIndex)) - } else if (provider === 'Trezor') { - await loginWithTrezor(loginCredentials, JSON.stringify(pathIndex)) - } else if (provider === 'WalletConnect'){ - await loginWithWalletConnectV2(loginCredentials) - } else { - console.log('Sign up not yet supported for this wallet provider') + async function setUserAccountBalances() { + try { + if (user?.value?.accounts) { + const { accounts } = user.value + const accountsWithBalances = await Promise.all( + accounts.map(async (account: Account) => { + const { address } = account + const balance = await getEthersBalance(address) + return { + ...account, + balance, } - await getUser() - } catch (error: any) { - throw new Error(error.message || 'There was an error logging in') - } + }) + ) + user.value.accounts = accountsWithBalances + } + } catch (error) { + throw new Error('Error setting user account balances') } + } - async function logout() { - // Loader - try { - loadingSessionLogout.value = true - await Session.signOut() - user.value = undefined - loadingSessionLogout.value = false - } catch (error) { - loadingSessionLogoutError.value = true - console.log('Error logging out user :>> ', error) - loadingSessionLogout.value = false - } - // TODO: Fix bug that doesn't allow you to log in without refreshing page after a user logs out - window.location.reload() + async function updateUserAgreement(agreed: boolean) { + const requestOptions = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ agreed }), } - - onMounted(async () => { - if (!initializeComposable.value) { - initializeComposable.value = true - // Loader - try { - loadingSessionLogin.value = true - const session = await Session.doesSessionExist() - if (session) await getUser() - await initializeWalletConnect() - loadingSessionLogin.value = false - } catch (error) { - loadingSessionLoginError.value = true - console.log('error getting user :>> ', error) - loadingSessionLogin.value = false - } - } - }) - - onUnmounted(() => { - initializeComposable.value = false - uninitializeWalletConnect() - }) + return await fetch( + `${usersUrl}/user/update-user-agreement/${user.value?.id}`, + requestOptions + ) + } - // TODO: Re-enable once we have a way to remove accounts in UI - // async function removeConnectedAccount() { - // if (!user?.value?.address) { - // alert('Please login first') - // } - // if (selectedAddress.value === primaryAddress.value) { - // return alert('Cannot remove primary account') - // } else if (ethersProviderList.includes(selectedProvider.value)) { - // const opts = { - // address: selectedAddress.value, - // currency: selectedCurrency.value, - // ownerAddress: primaryAddress.value, - // walletProvider: selectedProvider.value - // } - // const removeAccountResult = await removeAccount(opts) - // if (!removeAccountResult.error) { - // setSelectedAddress(removeAccountResult.data.address) - // removeAccountResult.data.accounts.forEach((account: Account) => { - // if (account.address === selectedAddress.value) { - // setSelectedProvider(account.walletProvider as ProviderString) - // setSelectedCurrency(account.currency as Currency) - // } - // }) - // } - // } - // } - - async function getEthersBalance(address: string) : Promise { - const balance = await provider.getBalance(address) - return parseFloat(ethers.utils.formatEther(balance)) - } + watch(user, async () => { + if (!user.value) return + await setUserAccountBalances() + }) - async function setUserAccountBalances() { - try { - if (user?.value?.accounts) { - const { accounts } = user.value - const accountsWithBalances = await Promise.all(accounts.map(async (account: Account) => { - const { address } = account - const balance = await getEthersBalance(address) - return { - ...account, - balance - } - })) - user.value.accounts = accountsWithBalances - } - } catch (error) { - throw new Error('Error setting user account balances') - } - } - - async function updatePrimaryAddress(updatedAddress: string) { - const userId = user?.value?.id - const requestOptions = { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ userId, updatedAddress }) - } - return await fetch(`${usersUrl}/user/update-primary-account`, requestOptions) - } - - async function updateUserAgreement(agreed: boolean) { - const requestOptions = { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ agreed }) - } - return await fetch(`${usersUrl}/user/update-user-agreement/${user.value?.id}`, requestOptions) - } - - return { - user: readonly(user), - loadingSessionLogin: readonly(loadingSessionLogin), - loadingSessionLoginError: readonly(loadingSessionLoginError), - loadingSessionLogout: readonly(loadingSessionLogout), - loadingSessionLogoutError: readonly(loadingSessionLogoutError), - addAccountToUser, - login, - logout, - updateUserAgreement, - } -} \ No newline at end of file + return { + user: readonly(user), + setUser, + updateUserAgreement, + } +} diff --git a/apps/web/src/composables/users(deprecated).ts b/apps/web/src/composables/users(deprecated).ts deleted file mode 100644 index 51c93fa9f..000000000 --- a/apps/web/src/composables/users(deprecated).ts +++ /dev/null @@ -1,85 +0,0 @@ -// import { ProviderString, UserWithAccountsAndOperators, ApiResponse, UserAnalyticsData } from '@casimir/types' -// import useEnvironment from '@/composables/environment' - -// const { usersUrl } = useEnvironment() - -// export default function useUsers() { -// async function checkIfPrimaryUserExists(provider: ProviderString, address: string): Promise { -// try { -// const requestOptions = { -// method: 'GET', -// headers: { -// 'Content-Type': 'application/json' -// } -// } -// const response = await fetch(`${usersUrl}/user/check-if-primary-address-exists/${provider}/${address}`, requestOptions) -// const { error, message, data } = await response.json() -// if (error) throw new Error(message) -// return { error, message, data } -// } catch (error: any) { -// throw new Error(error.message || 'Error checking if primary user exists') -// } -// } - -// async function checkIfSecondaryAddress(address: string) : Promise { -// try { -// const requestOptions = { -// method: 'GET', -// headers: { -// 'Content-Type': 'application/json' -// } -// } -// const response = await fetch(`${usersUrl}/user/check-secondary-address/${address}`, requestOptions) -// const { error, message, data } = await response.json() -// if (error) throw new Error(message) -// return { error, message, data } -// } catch (error: any) { -// throw new Error(error.message || 'Error checking if secondary address') -// } -// } - -// async function getMessage(address: string) { -// const response = await fetch(`${usersUrl}/auth/${address}`) -// const json = await response.json() -// const { message } = json -// return message -// } -// function setUser(newUser?: UserWithAccountsAndOperators) { -// user.value = newUser as UserWithAccountsAndOperators -// userAddresses.value = newUser?.accounts.map(account => account.address) as Array -// } - -// function setUserAnalytics(data?: UserAnalyticsData) { -// if (user.value?.id) { -// userAnalytics.value = data as UserAnalyticsData -// } else { -// const userAnalyticsInit = { -// oneMonth: { -// labels: [], -// data: [] -// }, -// sixMonth: { -// labels: [], -// data: [] -// }, -// oneYear: { -// labels: [], -// data: [] -// }, -// historical: { -// labels: [], -// data: [] -// } -// } -// userAnalytics.value = userAnalyticsInit -// } -// } - -// return { -// checkIfSecondaryAddress, -// checkIfPrimaryUserExists, -// getMessage, -// setUser, -// setUserAnalytics, -// } -// } \ No newline at end of file diff --git a/apps/web/src/composables/walletConnectV2.ts b/apps/web/src/composables/walletConnectV2.ts index d53e8834b..61d535add 100644 --- a/apps/web/src/composables/walletConnectV2.ts +++ b/apps/web/src/composables/walletConnectV2.ts @@ -3,12 +3,12 @@ import { providers } from 'ethers' import { EthereumProvider } from '@walletconnect/ethereum-provider' import { WalletConnectModal } from '@walletconnect/modal' import { PairingTypes, SessionTypes } from '@walletconnect/types' -import useAuth from '@/composables/auth' +import useSiwe from '@/composables/siwe' import useEthers from '@/composables/ethers' import useEnvironment from '@/composables/environment' import { CryptoAddress, LoginCredentials } from '@casimir/types' -const { createSiweMessage, signInWithEthereum } = useAuth() +const { createSiweMessage, signInWithEthereum } = useSiwe() const { walletConnectProjectId } = useEnvironment() const { getEthersBalance } = useEthers() @@ -147,10 +147,7 @@ export default function useWalletConnectV2() { } function uninitializeWalletConnect() { - cleanupFunctions.forEach((cleanup) => { - console.log('cleaning up') - cleanup() - }) + cleanupFunctions.forEach((cleanup) => cleanup()) cleanupFunctions = [] // Reset the array componentIsMounted.value = false } diff --git a/apps/web/src/composables/wallets.ts b/apps/web/src/composables/wallets.ts new file mode 100644 index 000000000..13bef696d --- /dev/null +++ b/apps/web/src/composables/wallets.ts @@ -0,0 +1,41 @@ +import { ref, readonly } from 'vue' +import { ProviderString } from '@casimir/types' + +const installedWallets = ref([] as ProviderString[]) + +export default function useWallets() { + async function detectInstalledWalletProviders() { + const ethereum = (window as any).ethereum + if (ethereum) { + // MetaMask, CoinbaseWallet, TrustWallet + if (ethereum.providers) { + // Iterate over ethereum.providers and check if MetaMask, CoinbaseWallet, TrustWallet + for (const provider of ethereum.providers) { + // Check if MetaMask + if (provider.isMetaMask) installedWallets.value.push('MetaMask') + // Check if CoinbaseWallet + if (provider.isCoinbaseWallet) installedWallets.value.push('CoinbaseWallet') + // Check if TrustWallet + if (provider.isTrust) installedWallets.value.push('TrustWallet') + } + } else if(ethereum.providerMap) { // This will not show Trust Wallet even if it is installed + // MetaMask & CoinbaseWallet + // Check if MetaMask + const isMetaMask = ethereum.providerMap.has('MetaMask') + if (isMetaMask) installedWallets.value.push('MetaMask') + // Check if CoinbaseWallet + const isCoinbaseWallet = ethereum.providerMap.has('CoinbaseWallet') + if (isCoinbaseWallet) installedWallets.value.push('CoinbaseWallet') + } else if (ethereum.isMetaMask) installedWallets.value.push('MetaMask') // Just MetaMask + else if (ethereum.isCoinbaseWallet) installedWallets.value.push('CoinbaseWallet') // Just CoinbaseWallet + else if (ethereum.isTrust) installedWallets.value.push('TrustWallet') // Just TrustWallet + // console.log('installedWallets.value :>> ', installedWallets.value) + } else { + console.log('No ethereum browser provider found') + } + } + return { + installedWallets: readonly(installedWallets), + detectInstalledWalletProviders + } +} \ No newline at end of file diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 3d8575357..a7a31d8d4 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -3,54 +3,54 @@ @tailwind utilities; :root { -font-family: 'IBM Plex Sans', sans-serif; -font-synthesis: none; + font-family: "IBM Plex Sans", sans-serif; + font-synthesis: none; -max-width: 1400px; -margin: auto; -text-rendering: optimizeLegibility; --webkit-font-smoothing: antialiased; --moz-osx-font-smoothing: grayscale; --webkit-text-size-adjust: 100%; + max-width: 1400px; + margin: auto; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; } :root::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 319px; - background-color: #000; - z-index: -1; - } + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 319px; + background-color: #000; + z-index: -1; +} -.tooltip_container{ - position: relative; +.tooltip_container { + position: relative; } .tooltip_container:hover .tooltip { - visibility: visible; - opacity: 1; + visibility: visible; + opacity: 1; } .tooltip { - position: absolute; - top: 110%; - border-radius: 3px; - background-color: rgba(0,0,0,0.75); - color: white; - font-size: 12px; - line-height: 14px; - padding: 6px 8px; - visibility: hidden; - white-space: normal; - opacity: 0; - transition: opacity 0.4s ease-in-out; - z-index: 5; + position: absolute; + top: 110%; + border-radius: 3px; + background-color: rgba(49, 48, 54, 0.824); + color: rgb(255, 255, 255); + font-size: 12px; + font-weight: 500; + line-height: 14px; + padding: 6px 8px; + visibility: hidden; + white-space: normal; + opacity: 0; + transition: opacity 0.4s ease-in-out; + z-index: 5; } - .skeleton_box { position: absolute; top: 0; @@ -71,4 +71,4 @@ text-rendering: optimizeLegibility; 100% { background-position: -200% 0; } -} \ No newline at end of file +} diff --git a/apps/web/src/layouts/default-layout.vue b/apps/web/src/layouts/default-layout.vue index fcd52ee2a..7261ecf66 100644 --- a/apps/web/src/layouts/default-layout.vue +++ b/apps/web/src/layouts/default-layout.vue @@ -1,105 +1,40 @@ @/composables/user \ No newline at end of file + \ No newline at end of file diff --git a/apps/web/src/pages/operators/Operator.vue b/apps/web/src/pages/operators/Operator.vue index 2f0b4cc7d..f6e571435 100644 --- a/apps/web/src/pages/operators/Operator.vue +++ b/apps/web/src/pages/operators/Operator.vue @@ -2,18 +2,22 @@ import { onMounted, ref, watch } from 'vue' import VueFeather from 'vue-feather' import { ProviderString } from '@casimir/types' +import useAuth from '@/composables/auth' +import useEthers from '@/composables/ethers' import useFiles from '@/composables/files' import useFormat from '@/composables/format' -import useUser from '@/composables/user' import useOperators from '@/composables/operators' +import useUser from '@/composables/user' import { UserWithAccountsAndOperators} from '@casimir/types' +const { loadingSessionLogin } = useAuth() +const { detectActiveWalletAddress } = useEthers() const { exportFile } = useFiles() const { convertString } = useFormat() -const { user, loadingSessionLogin } = useUser() +const { user } = useUser() // Form inputs -const selectedWallet = ref({address: '', wallet_provider: ''}) +const selectedWallet = ref<{address: string, walletProvider: ProviderString}>({address: '', walletProvider: ''}) const openSelectWalletOptions = ref(false) const onSelectWalletBlur = () => { setTimeout(() =>{ @@ -37,7 +41,7 @@ const itemsPerPage = ref(10) const currentPage = ref(1) const totalPages = ref(1) const searchInput = ref('') -const selectedHeader = ref('wallet_provider') +const selectedHeader = ref('walletProvider') const selectedOrientation = ref('ascending') const operatorTableHeaders = ref( [ @@ -139,7 +143,7 @@ watch(registeredOperators, () => { watch(openAddOperatorModal, () =>{ if(openAddOperatorModal.value){ - selectedWallet.value = {address: user.value?.address as string, wallet_provider: user.value?.walletProvider as string} + selectedWallet.value = {address: user.value?.address as string, walletProvider: user.value?.walletProvider as ProviderString} } }) @@ -180,7 +184,7 @@ const filterData = () => { filteredDataArray = tableData.value.filter((item: any) => { return ( // Might need to modify to match types each variable - // item.wallet_provider?.toLowerCase().includes(searchTerm) + // item.walletProvider?.toLowerCase().includes(searchTerm) true ) }) @@ -223,9 +227,17 @@ watch([selectedWallet, selectedOperatorID, selectedPublicNodeURL, selectedCollat }) async function submitRegisterOperatorForm() { + const selectedAddress = selectedWallet.value.address + const selectedProvider = selectedWallet.value.walletProvider + + const activeAddress = await detectActiveWalletAddress(selectedProvider) + if (activeAddress !== selectedAddress) { + return alert(`The account you selected is not the same as the one that is active in your ${selectedProvider} wallet. Please open your browser extension and select the account that you want to log in with.`) + } + try { await registerOperatorWithCasimir({ - walletProvider: selectedWallet.value.wallet_provider as ProviderString, + walletProvider: selectedWallet.value.walletProvider as ProviderString, address: selectedWallet.value.address, operatorId: parseInt(selectedOperatorID.value), collateral: selectedCollateral.value, @@ -239,7 +251,7 @@ async function submitRegisterOperatorForm() { if (selectedWallet.value.address === '') { const primaryAccount = user.value?.accounts.find(item => { item.address === user.value?.address}) - selectedWallet.value = {address: primaryAccount?.address as string, wallet_provider: primaryAccount?.walletProvider as string} + selectedWallet.value = {address: primaryAccount?.address as string, walletProvider: primaryAccount?.walletProvider as ProviderString} } selectedOperatorID.value = '' selectedPublicNodeURL.value = '' @@ -365,7 +377,7 @@ watch([loadingSessionLogin || loadingInitializeOperators], () =>{ >