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 <sebastinez@me.com>
This commit is contained in:
Sebastian Martinez
2022-05-20 13:10:53 +02:00
committed by Alexis Sellier
parent ab3c8f5657
commit be0ff349df
9 changed files with 149 additions and 36 deletions
+19
View File
@@ -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");
});
});
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -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");
+1 -1
View File
@@ -8,7 +8,7 @@ module.exports = (on, config) => {
options,
viteConfig: {
configFile: path.resolve(__dirname, '..', '..', 'vite.config.ts'),
logLevel: "warn"
logLevel: "silent"
},
});
});
+82 -9
View File
@@ -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";
}
}
+3 -3
View File
@@ -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>
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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>
+40 -19
View File
@@ -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 }));
}