From be0ff349df2cc3f45a1e8f610ee8ccd5ce23d5f5 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 23 Mar 2022 21:00:35 +0100 Subject: [PATCH] Move signer into session store The Metamask and WalletConnect signer are updated one any account change, but currently we store them only in the static config object. This commit moves this to the session store to improve this behaviour Signed-off-by: Sebastian Martinez --- cypress/integration/connect.spec.ts | 19 ++++++ cypress/integration/home.spec.ts | 2 +- cypress/integration/project.spec.ts | 2 +- cypress/plugins/index.cjs | 2 +- cypress/support/index.ts | 91 ++++++++++++++++++++++++++--- src/base/faucet/Index.svelte | 6 +- src/base/faucet/Withdraw.svelte | 2 +- src/base/seeds/View.svelte | 2 +- src/session.ts | 59 +++++++++++++------ 9 files changed, 149 insertions(+), 36 deletions(-) create mode 100644 cypress/integration/connect.spec.ts diff --git a/cypress/integration/connect.spec.ts b/cypress/integration/connect.spec.ts new file mode 100644 index 000000000..02e694966 --- /dev/null +++ b/cypress/integration/connect.spec.ts @@ -0,0 +1,19 @@ +/// +import { MockExtensionProvider } from "../support"; + +describe("MetaMask", () => { + it("Lets user connect with Metamask provider", () => { + cy.visit("/faucet", { + onBeforeLoad(win) { + win.ethereum = new MockExtensionProvider("rinkeby", "0x3256a804085C24f3451cAb2C98a37e16DEEc5721"); + } + }); + cy.get("button.connect").click(); + cy.get("button.secondary").click(); + cy.get("button.address").should("contain", "3256 – 5721"); + cy.window().then((win) => { + win.ethereum.changeAccount("0xd3b5586D15140B6f793b260fd90588A0dAefc5B6"); + }); + cy.get("button.address").should("contain", "d3b5 – c5B6"); + }); +}); diff --git a/cypress/integration/home.spec.ts b/cypress/integration/home.spec.ts index c916024f0..cc19d42b7 100644 --- a/cypress/integration/home.spec.ts +++ b/cypress/integration/home.spec.ts @@ -17,7 +17,7 @@ describe("Landing page", () => { cy.intercept("https://willow.radicle.garden:8777/v1/projects", { fixture: "projectList.json" }); cy.visit("/", { onBeforeLoad(win) { - win.ethereum = new MockExtensionProvider(); + win.ethereum = new MockExtensionProvider("homestead", "0x3256a804085C24f3451cAb2C98a37e16DEEc5721"); }, }); cy.get("div.card-label") diff --git a/cypress/integration/project.spec.ts b/cypress/integration/project.spec.ts index 958e58d95..78817a025 100644 --- a/cypress/integration/project.spec.ts +++ b/cypress/integration/project.spec.ts @@ -21,7 +21,7 @@ describe("Project view", () => { it("Page header", () => { cy.visit("/seeds/willow.radicle.garden/bright-forest-protocol", { onBeforeLoad(win) { - win.ethereum = new MockExtensionProvider(); + win.ethereum = new MockExtensionProvider("homestead", "0x3256a804085C24f3451cAb2C98a37e16DEEc5721"); }, }); cy.get("div.title").contains("bright-forest-protocol"); diff --git a/cypress/plugins/index.cjs b/cypress/plugins/index.cjs index d4edfb687..6799380a2 100644 --- a/cypress/plugins/index.cjs +++ b/cypress/plugins/index.cjs @@ -8,7 +8,7 @@ module.exports = (on, config) => { options, viteConfig: { configFile: path.resolve(__dirname, '..', '..', 'vite.config.ts'), - logLevel: "warn" + logLevel: "silent" }, }); }); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 04cb7c542..e28084d3c 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -1,31 +1,92 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import '@cypress/code-coverage/support'; +import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; import '@testing-library/cypress/add-commands'; -import { ethers } from 'ethers'; +import { BigNumber, ethers } from 'ethers'; declare global { interface Window { ethereum: any; + localStorage: Storage; } } export class MockExtensionProvider extends ethers.providers.BaseProvider { isMetaMask = true; + currentAddress: string; - constructor() { - super({ name: "homestead", chainId: 1 }); + constructor(network: ethers.providers.Networkish, address: string) { + super(network); + this.currentAddress = address; console.log("Creating Mock provider"); } + + get ready(): Promise { + return Promise.resolve(this.network); + } + + getSigner(addressOrIndex?: string | number): JsonRpcSigner { + return new JsonRpcSigner({}, this as unknown as JsonRpcProvider, addressOrIndex); + } + + changeAccount(address: string): void { + this.currentAddress = address; + this.emit("accountsChanged", [address]); + } + async request({ method, params }: { method: string; params: any }): Promise { switch (method) { case 'eth_chainId': - return "1"; + return this.network.chainId; case 'net_version': - return "1"; + return this.network.chainId; case 'eth_call': return resolveEthCall(params); + case 'eth_accounts': + return [this.currentAddress]; + case 'eth_requestAccounts': + return [this.currentAddress]; case 'eth_getCode': return "0x"; + case 'eth_blockNumber': + return BigNumber.from(1); + case 'eth_estimateGas': + return BigNumber.from(21000); + case 'eth_sendTransaction': + return "0x8829dea7e20ebcf6dbfd942e3613d7ac49b9aef3ecbed396acfc5901713f5983"; + case 'eth_getTransactionByHash': + return { + hash: "0x8829dea7e20ebcf6dbfd942e3613d7ac49b9aef3ecbed396acfc5901713f5983", + to: "0x0000000000000000000000000000000000000000", + from: "0x0000000000000000000000000000000000000000", + nonce: 123, + gasLimit: BigNumber.from(21000), + gasPrice: BigNumber.from(40), + data: "0x", + value: BigNumber.from(0), + chainId: 4, + }; + case 'eth_getTransactionCount': + return BigNumber.from(123); + case 'eth_getTransactionReceipt': + return { + to: '0x01139F82659A3bD56D1f051D57D4Bc96a3b9Ef05', + from: '0x00192Fb10dF37c9FB26829eb2CC623cd1BF599E8', + contractAddress: null, + transactionIndex: 338, + gasUsed: BigNumber.from('0x5208'), + logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + blockHash: '0xdbe7ca0dd26310cd5415202c67467d46795cfec922dbb47e4cc4ff388e7856d2', + transactionHash: '0x8829dea7e20ebcf6dbfd942e3613d7ac49b9aef3ecbed396acfc5901713f5983', + logs: [], + blockNumber: 14451272, + confirmations: 53, + cumulativeGasUsed: BigNumber.from('0x01b11d57'), + effectiveGasPrice: BigNumber.from('0x0dffd03ca0'), + status: 1, + type: 2, + byzantium: true + }; default: console.log("Unknown method", method); break; @@ -34,16 +95,28 @@ export class MockExtensionProvider extends ethers.providers.BaseProvider { } function resolveEthCall(params: { to: string; data: string }[]): string { + const [{ to, data }] = params; + // Get Resolver - if (params[0].to === "0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e") { + if (to === "0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e") { return "0x0000000000000000000000004976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41"; // Get ENS Attributes - } else if (params[0].to === "0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41" && params[0].data === "0x59d1d43c567c364804de7bbedb53f583e483f6b73513fd2f44299e281024e4719da0b332000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066176617461720000000000000000000000000000000000000000000000000000") { + } else if (to === "0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41" && data === "0x59d1d43c567c364804de7bbedb53f583e483f6b73513fd2f44299e281024e4719da0b332000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066176617461720000000000000000000000000000000000000000000000000000") { return "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001f68747470733a2f2f636c6f7564686561642e696f2f6176617461722e706e6700"; // Get Org informations - } else if (params[0].to === "0x8152237402e0f194176154c3a6ea1eb99b611482" && params[0].data === "0x8da5cb5b") { + } else if (to === "0x8152237402e0f194176154c3a6ea1eb99b611482" && data === "0x8da5cb5b") { return "0x000000000000000000000000ceab094641905c209cc796fc8037dd9ecc87ca2f"; - } else { + // Get Token Balance + } else if (to === "0x31c8eacbffdd875c74b94b077895bd78cf1e64a3" && data === "0x70a082310000000000000000000000003256a804085c24f3451cab2c98a37e16deec5721") { + return "0x00000000000000000000000000000000000000000000000246DDF97976680000"; + // getMaxWithdrawAmount + } else if (to === "0x9aa75397ed632a3060acb5de7f96e2457bceed8d" && data === "0xf516440c") { + return "0x000000000000000000000000000000000000000000000001D7D843DC3B480000"; + // Get resolved address from ENS + } else if (to === "0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41") { return "0x000000000000000000000000394b920c5d39e0ca40fca2871569b6b90d750c7c"; + // Return 0 for token balances else + } else { + return "0x0000000000000000000000000000000000000000000000000000000000000000"; } } diff --git a/src/base/faucet/Index.svelte b/src/base/faucet/Index.svelte index c7df94943..2b5abced6 100644 --- a/src/base/faucet/Index.svelte +++ b/src/base/faucet/Index.svelte @@ -32,7 +32,7 @@ if (!amount || amount === "0") { return [false, "Not able to withdraw zero tokens"]; } if (toWei(amount).gt(maxWithdrawAmount)) return [false, `Reduce amount, max withdrawal is ${formatEther(maxWithdrawAmount)}`]; let currentTime = new Date().getTime(); - let timelock = await calculateTimeLock(amount, $session.signer, config); + let timelock = await calculateTimeLock(amount, $session.config.signer, config); // Converting a 10 digit to 13 digit timestamp by multiplying by 1000 // since JS doesn't display a correct Date string when passing a 10 digit timestamp. let nextAvailableWithdraw = lastWithdrawal.add(timelock).mul(1000); @@ -48,8 +48,8 @@ } $: if ($session) { - getMaxWithdrawAmount($session.signer, config).then(x => maxWithdrawAmount = x); - lastWithdrawalByUser($session.signer, config).then(x => lastWithdrawal = x); + getMaxWithdrawAmount($session.config.signer, config).then(x => maxWithdrawAmount = x); + lastWithdrawalByUser($session.config.signer, config).then(x => lastWithdrawal = x); } diff --git a/src/base/faucet/Withdraw.svelte b/src/base/faucet/Withdraw.svelte index b67166ef7..68cdaac51 100644 --- a/src/base/faucet/Withdraw.svelte +++ b/src/base/faucet/Withdraw.svelte @@ -25,7 +25,7 @@ try { if ($session) { state.status = Status.Signing; - const tx = await withdraw(amount, $session.signer, config); + const tx = await withdraw(amount, $session.config.signer, config); state.status = Status.Pending; await tx.wait(); state.status = Status.Success; diff --git a/src/base/seeds/View.svelte b/src/base/seeds/View.svelte index d008cb969..7b48dde38 100644 --- a/src/base/seeds/View.svelte +++ b/src/base/seeds/View.svelte @@ -109,7 +109,7 @@
- {#if session?.signer} + {#if session?.config.signer} {#if siweSession}
Signed in as diff --git a/src/session.ts b/src/session.ts index 7a322147f..5a287ed22 100644 --- a/src/session.ts +++ b/src/session.ts @@ -8,7 +8,6 @@ import type { TypedDataSigner } from '@ethersproject/abstract-signer'; import type { WalletConnectSigner } from "./WalletConnectSigner"; import * as ethers from "ethers"; import type { SeedSession } from "./siwe"; -import { unixTime } from "./utils"; export enum Connection { Disconnected, @@ -23,7 +22,21 @@ export type TxState = | { state: 'fail'; hash: string; blockHash: string; blockNumber: number; error: string } | null; -export type Signer = ethers.Signer & TypedDataSigner | WalletConnectSigner; +export type Signer = ethers.Signer & TypedDataSigner | WalletConnectSigner | null; + +// Defines the type of signer we are using in the current session. +// Allows us to guard certain functionality for a specific signer. +enum SignerType { + WalletConnect, + MetaMask +} + +// Definitions made in `Config` that need to be part of the session, +// to provide reactivity based on events i.e. accountsChanged +export interface SignerConfig { + signer: Signer; + type: SignerType; +} export type State = { connection: Connection.Disconnected } @@ -32,7 +45,7 @@ export type State = export interface Session { address: string; - signer: Signer | null; + config: SignerConfig; siwe: { [key: string]: SeedSession }; tokenBalance: BigNumber | null; // `null` means it isn't loaded yet. tx: TxState; @@ -47,7 +60,7 @@ export interface Store extends Readable { setTxSigning(): void; setTxPending(tx: TransactionResponse): void; setTxConfirmed(tx: TransactionReceipt): void; - setChangedAccount(address: string, config: Config): void; + setChangedAccount(address: string): void; } export const loadState = (initial: State): Store => { @@ -62,8 +75,9 @@ export const loadState = (initial: State): Store => { // Re-connect using previous session. if (config.metamask.connected) { const metamask = config.metamask.session; + const signerConfig: SignerConfig = { signer: config.signer, type: SignerType.MetaMask }; const tokenBalance: BigNumber = await config.token.balanceOf(metamask.address); - const session = { tokenBalance, tx: null, siwe, signer: config.metamask.signer, address: metamask.address }; + const session = { tokenBalance, tx: null, siwe, config: signerConfig, address: metamask.address }; store.set({ connection: Connection.Connected, session }); config.setSigner(config.metamask.signer); @@ -87,13 +101,14 @@ export const loadState = (initial: State): Store => { config.walletConnect.state.set({ state: "close" }); const tokenBalance: BigNumber = await config.token.balanceOf(address); - const session = { address, signer: config.metamask.signer, siwe, tokenBalance, tx: null }; + const signerConfig = { signer: config.signer, type: SignerType.MetaMask }; + const session = { config: signerConfig, siwe, address, tokenBalance, tx: null }; store.set({ connection: Connection.Connected, session, }); - saveSession({ ...session }); + saveMetamaskSession(session); } catch (e) { console.error(e); } @@ -107,9 +122,10 @@ export const loadState = (initial: State): Store => { await config.walletConnect.client.connect(); console.log("WalletConnect: connected."); + const signerConfig: SignerConfig = { signer: config.signer, type: SignerType.WalletConnect }; const address = await signer.getAddress(); const tokenBalance: BigNumber = await config.token.balanceOf(address); - const session = { address, signer, siwe, tokenBalance, tx: null }; + const session = { address, config: signerConfig, siwe, tokenBalance, tx: null }; const network = ethers.providers.getNetwork( signer.walletConnect.chainId ); @@ -125,6 +141,8 @@ 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]); // Check the current chainId, and request Metamask to change, or reload the window to get the correct chain. @@ -163,7 +181,7 @@ export const loadState = (initial: State): Store => { switch (s.connection) { case Connection.Connected: s.session.siwe[id] = session; - saveSession(s.session); + saveSeedSession(s.session); return s; default: @@ -179,7 +197,7 @@ export const loadState = (initial: State): Store => { // If the token balance is loaded, we can update it, otherwise // we let it finish loading. s.session.tokenBalance = s.session.tokenBalance.add(n); - saveSession(s.session); + saveMetamaskSession(s.session); } return s; }); @@ -257,18 +275,20 @@ export const loadState = (initial: State): Store => { }); }, - setChangedAccount: (address: string, config: Config) => { + setChangedAccount: (address: string) => { store.update(s => { switch (s.connection) { case Connection.Connected: // In case of locking Metamask the accountsChanged event returns undefined. // To prevent out of sync state, the wallet gets disconnected. if (address === undefined) { + // This ends with a window reload. disconnectMetamask(); } else { s.session.address = address; - s.session.signer = config.signer; - saveSession(s.session); + // 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); } return s; default: @@ -301,7 +321,7 @@ window.ethereum?.on("accountsChanged", async ([address]: string) => { export async function changeAccounts(address: string): Promise { const config = await getConfig(); - state.setChangedAccount(address, config); + state.setChangedAccount(address); state.refreshBalance(config); } @@ -314,7 +334,7 @@ export function loadSeedSessions(): { [key: string]: SeedSession } { // We only keep the sessions that are still valid, and remove expired ones from `localStorage`. // For a session to be valid the expiration time has to be bigger or equal than the current time. const activeSessions = Object.fromEntries(Object.entries(siwe).filter(([, value]) => { - return value.expirationTime >= unixTime(); + return new Date(value.expirationTime) >= new Date(); })); window.localStorage.setItem("siwe", JSON.stringify({ ...activeSessions })); @@ -358,9 +378,10 @@ export function disconnectWallet(config: Config): void { disconnectMetamask(); } -function saveSession(session: Session): void { - const { address, tokenBalance, tx, siwe } = session; +function saveSeedSession(session: Session): void { + window.localStorage.setItem("siwe", JSON.stringify(session.siwe)); +} - window.localStorage.setItem("metamask", JSON.stringify({ address, tokenBalance, tx })); - window.localStorage.setItem("siwe", JSON.stringify(siwe)); +function saveMetamaskSession(session: Session): void { + window.localStorage.setItem("metamask", JSON.stringify({ address: session.address, tokenBalance: null, tx: null, config: null })); }