Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Move signer into session store
Sebastian Martinez committed 4 years ago
commit be0ff349df2cc3f45a1e8f610ee8ccd5ce23d5f5
parent ab3c8f565794ecddd0c28bbb1a3d8c7142c7b3d9
9 files changed +149 -36
added cypress/integration/connect.spec.ts
@@ -0,0 +1,19 @@
+
/// <reference types="cypress" />
+
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");
+
  });
+
});
modified 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")
modified 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");
modified 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"
      },
    });
  });
modified 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<ethers.providers.Network> {
+
    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<any> {
    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";
  }
}
modified 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);
  }
</script>

modified 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;
modified src/base/seeds/View.svelte
@@ -109,7 +109,7 @@
      </span>
      <!-- User Session -->
      <div class="siwe">
-
        {#if session?.signer}
+
        {#if session?.config.signer}
          {#if siweSession}
            <div class="session-info">
              <span class="signed-in text-small">Signed in as</span>
modified 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<State> {
  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<void> {
  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 }));
}