diff --git a/cypress/integration/connect.spec.ts b/cypress/integration/connect.spec.ts index 02e694966..fdf5fe6ac 100644 --- a/cypress/integration/connect.spec.ts +++ b/cypress/integration/connect.spec.ts @@ -8,6 +8,8 @@ describe("MetaMask", () => { win.ethereum = new MockExtensionProvider("rinkeby", "0x3256a804085C24f3451cAb2C98a37e16DEEc5721"); } }); + cy.intercept("POST", "https://api.thegraph.com/subgraphs/name/radicle-dev/radicle-orgs-rinkeby", { data: { safe: null } } ); + cy.intercept("https://gateway.ceramic.network/api/v0/streams", new Blob([])); cy.get("button.connect").click(); cy.get("button.secondary").click(); cy.get("button.address").should("contain", "3256 – 5721"); diff --git a/src/App.svelte b/src/App.svelte index c5b06358b..ebc121b4b 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -20,7 +20,7 @@ const loadConfig = getConfig().then(async cfg => { if ($state.connection === Connection.Connected) { - state.refreshBalance(cfg); + state.refreshBalance(); } else if ($state.connection === Connection.Disconnected) { // Update the session state if we're already connected to WalletConnect // from a previous session. diff --git a/src/base/vesting/vesting.ts b/src/base/vesting/vesting.ts index c065e5a9a..6a95b4095 100644 --- a/src/base/vesting/vesting.ts +++ b/src/base/vesting/vesting.ts @@ -26,7 +26,7 @@ export async function withdrawVested(address: string, config: Config): Promise { connectWalletConnect(config: Config): Promise; updateBalance(n: BigNumber): void; connectSeed(seed: { id: string; session: SeedSession }): void; - refreshBalance(config: Config): Promise; + refreshBalance(): Promise; setTxSigning(): void; setTxPending(tx: TransactionResponse): void; setTxConfirmed(tx: TransactionReceipt): void; - setChangedAccount(address: string): void; + setChangedAccount(address: string, signer: Signer): void; } +// This is a buffer for the Config class to be available in this file, without making it a store just yet. +// TODO: We should be thinking on making config a store, similar to session, to be more accesible. +let sessionConfig: Config | null = null; + export const loadState = (initial: State): Store => { const store = writable(initial); + // Loads from localStorage all active seed sessions. const siwe = loadSeedSessions(); return { subscribe: store.subscribe, connectMetamask: async (config: Config) => { + // TODO: Updates at the moment the local sessionConfig variable, will be removed eventually + sessionConfig = config; assert(config.metamask.signer); + // We use config.metamask.signer here, because config.signer is still null on page reload. + const signer = config.metamask.signer; // Re-connect using previous session. if (config.metamask.connected) { const metamask = config.metamask.session; - const signerConfig: SignerConfig = { signer: config.signer, type: SignerType.MetaMask }; + const signerConfig: SignerConfig = { signer, type: SignerType.MetaMask }; const tokenBalance: BigNumber = await config.token.balanceOf(metamask.address); - const session = { tokenBalance, tx: null, siwe, config: signerConfig, address: metamask.address }; + const session = { address: metamask.address, config: signerConfig, siwe, tokenBalance, tx: null }; store.set({ connection: Connection.Connected, session }); - config.setSigner(config.metamask.signer); + config.setSigner(signer); return; } @@ -91,18 +100,17 @@ export const loadState = (initial: State): Store => { store.set({ connection: Connection.Connecting }); await window.ethereum.request({ method: 'eth_requestAccounts' }); - - const signer = config.metamask.signer; const address = await signer.getAddress(); config.setSigner(signer); try { + // We close here any walletConnect session that may be open, before establishing a new Metamask connection. config.walletConnect.state.set({ state: "close" }); const tokenBalance: BigNumber = await config.token.balanceOf(address); - const signerConfig = { signer: config.signer, type: SignerType.MetaMask }; - const session = { config: signerConfig, siwe, address, tokenBalance, tx: null }; + const signerConfig = { signer, type: SignerType.MetaMask }; + const session = { address, config: signerConfig, siwe, tokenBalance, tx: null }; store.set({ connection: Connection.Connected, @@ -115,15 +123,18 @@ export const loadState = (initial: State): Store => { }, connectWalletConnect: async (config: Config) => { + // TODO: Updates at the moment the local sessionConfig variable, will be removed eventually + sessionConfig = config; store.set({ connection: Connection.Connecting }); + // We fetch the walletConnect signer here, because config.signer is still null on page reload. const signer = config.getWalletConnectSigner(); try { await config.walletConnect.client.connect(); console.log("WalletConnect: connected."); - const signerConfig: SignerConfig = { signer: config.signer, type: SignerType.WalletConnect }; - const address = await signer.getAddress(); + let address = await signer.getAddress(); + const signerConfig: SignerConfig = { signer, type: SignerType.WalletConnect }; const tokenBalance: BigNumber = await config.token.balanceOf(address); const session = { address, config: signerConfig, siwe, tokenBalance, tx: null }; const network = ethers.providers.getNetwork( @@ -141,10 +152,15 @@ export const loadState = (initial: State): Store => { } try { - // When the WalletConnect session is updated, we need to update the config signer. - config.getWalletConnectSigner(); // We only change accounts if the address has been changed, to avoid unnecessary refreshing. - if (address !== accounts[0]) changeAccounts(accounts[0]); + if (address !== accounts[0]) { + // When session_update address doesn't match the signer address, we update the signer and pass it to changeAccounts to update the session. + const signer = config.getWalletConnectSigner(); + changeAccounts(accounts[0], signer); + + // We update the address variable in scope of the connectWalletConnect function async here, to be used in the future when session_updates triggers. + signer.getAddress().then(a => address = a); + } // Check the current chainId, and request Metamask to change, or reload the window to get the correct chain. if (chainId !== config.network.chainId) { if (isMetamaskInstalled()) { @@ -203,13 +219,13 @@ export const loadState = (initial: State): Store => { }); }, - refreshBalance: async (config: Config) => { + refreshBalance: async () => { const state = get(store); - assert(state.connection === Connection.Connected); + assert(state.connection === Connection.Connected && sessionConfig); // We should be connected and have a config created. const addr = state.session.address; try { - const tokenBalance: BigNumber = await config.token.balanceOf(addr); + const tokenBalance: BigNumber = await sessionConfig.token.balanceOf(addr); state.session.tokenBalance = tokenBalance; store.set(state); @@ -275,7 +291,7 @@ export const loadState = (initial: State): Store => { }); }, - setChangedAccount: (address: string) => { + setChangedAccount: (address: string, signer: Signer) => { store.update(s => { switch (s.connection) { case Connection.Connected: @@ -286,6 +302,7 @@ export const loadState = (initial: State): Store => { disconnectMetamask(); } else { s.session.address = address; + s.session.config.signer = signer; // We only save the session to localStorage if we use a MetaMask signer // WalletConnect does their own session persistance. if (s.session.config.type === SignerType.MetaMask) saveMetamaskSession(s.session); @@ -299,6 +316,7 @@ export const loadState = (initial: State): Store => { }; }; +// Initializes the session state on page load or hard refresh. export const state = loadState({ connection: Connection.Disconnected }); export const session = derived(state, s => { @@ -316,13 +334,18 @@ window.ethereum?.on('chainChanged', () => { // Updates state when user changes accounts window.ethereum?.on("accountsChanged", async ([address]: string) => { - changeAccounts(address); + // Only allow user to change accounts with Metamask if they are connected with Metamask. + if (get(session)?.config.type !== SignerType.MetaMask) { + return; + // TODO: Temporary use case of sessionConfig, will eventually be removed, if moving config to a svelte store. + } else if (sessionConfig) { + changeAccounts(address, sessionConfig.metamask.signer); + } }); -export async function changeAccounts(address: string): Promise { - const config = await getConfig(); - state.setChangedAccount(address); - state.refreshBalance(config); +export async function changeAccounts(address: string, signer: Signer): Promise { + state.setChangedAccount(address, signer); + state.refreshBalance(); } export function loadSeedSessions(): { [key: string]: SeedSession } { diff --git a/src/siwe.ts b/src/siwe.ts index 0cf33cd54..69de857b2 100644 --- a/src/siwe.ts +++ b/src/siwe.ts @@ -4,7 +4,6 @@ import { Request, type Host } from '@app/api'; import type { Config } from "@app/config"; import { connectSeed } from "@app/session"; import type { Seed } from "@app/base/seeds/Seed"; -import { ethers } from "ethers"; export interface SeedSession { domain: string; @@ -48,12 +47,8 @@ export async function signInWithEthereum(seed: Seed, config: Config): Promise<{ } const address = await config.signer.getAddress(); - // We convert the address to a checksummed address, since WalletConnect returns a lowercase address. - // We need a checksummed address to verify it on the seed node. - const checksummedAddress = ethers.utils.getAddress(address); - const result = await createUnauthorizedSession(seed.api); - const message = createSiweMessage(seed, checksummedAddress, result.nonce, config); + const message = createSiweMessage(seed, address, result.nonce, config); const signature = await config.signer.signMessage(message); const auth: {