Improve session code

This commit is contained in:
Alexis Sellier
2021-05-03 18:17:18 +02:00
parent 451ae1fdc3
commit 10e2847d5d
17 changed files with 283 additions and 133 deletions
+3 -3
View File
@@ -38,13 +38,13 @@
<!-- Loading wallet -->
{:then config}
<div class="app">
<Header/>
<Header session={$session} {config} />
<div class="wrapper">
<Router url="{url}">
<Route path="vesting">
<Vesting {config} />
<Vesting {config} session={$session} />
</Route>
<Register {config} {query} />
<Register {config} {query} session={$session} />
<Orgs {config} {query} />
</Router>
</div>
+5 -3
View File
@@ -1,21 +1,23 @@
<script lang="typescript">
import { derived } from "svelte/store";
import { Connection, session, connectWallet } from "./session";
import { Connection } from "@app/session";
import { state } from '@app/session';
export let config;
export let caption = "Connect";
export let className = "";
export let style = "";
let walletUnavailable = !window.ethereum;
$: connecting = $session.connection === Connection.Connecting;
$: connecting = $state.connection === Connection.Connecting;
</script>
<style>
</style>
<button
on:click={connectWallet}
on:click={() => state.connect(config)}
{style}
class="connect {className}"
disabled={connecting || walletUnavailable}
+10 -4
View File
@@ -1,20 +1,26 @@
<script lang="typescript">
// TODO: Shorten tx hash
// TODO: Link to correct network on etherscan
// TODO: There's a bug where sometimes on first load, the 'Connect' button
// won't display the address even though we're connected.
import { derived } from "svelte/store";
import { ethers } from "ethers";
import { link } from "svelte-routing";
import { formatBalance, formatAddress } from "@app/utils";
import { error, Failure } from '@app/error';
import { session, disconnectWallet } from "@app/session";
import { disconnectWallet } from "@app/session";
import type { Session } from '@app/session';
import Logo from './Logo.svelte';
import Connect from './Connect.svelte';
export let session: Session | null;
export let config;
let sessionButton = null;
let sessionButtonHover = false;
$: address = $session.address;
$: tokenBalance = $session.tokenBalance;
$: address = session && session.address;
$: tokenBalance = session && session.tokenBalance;
</script>
<style>
@@ -99,7 +105,7 @@
{/if}
</button>
{:else}
<Connect className="small" />
<Connect className="small" {config} />
{/if}
</div>
</header>
+1 -1
View File
@@ -37,7 +37,7 @@
{/if}
<div class:modal-floating={floating}>
<div class="modal" class:error={error}>
<div class="modal" class:error>
<div class="modal-title">
{#if error}
Error
+46 -22
View File
@@ -1,36 +1,38 @@
<script lang="typescript">
import { createEventDispatcher } from 'svelte';
import { session } from '@app/session';
import Modal from '@app/Modal.svelte';
import { Org } from '@app/base/orgs/Org';
import type { Config } from '@app/config';
export let config;
export let owner;
export let config: Config;
export let owner: string;
enum State {
Idle,
Waiting,
Signing,
Pending,
Success,
}
let state = State.Idle;
let error = null;
let org = null;
const dispatch = createEventDispatcher();
const createOrg = async () => {
state = State.Waiting;
state = State.Signing;
console.log("creating org");
try {
let tx = await Org.create(owner, config);
state = State.Pending;
let receipt = await tx.wait();
console.log(receipt);
let org = Org.fromReceipt(receipt);
console.log(org);
org = Org.fromReceipt(receipt);
state = State.Success;
} catch (e) {
state = State.Idle;
console.error(e);
state = State.Idle;
error = e;
}
};
@@ -38,23 +40,45 @@
<Modal floating {error} on:close>
<span slot="title">
Create an Org
{#if !org}
Create an Org
{:else}
Success
{/if}
</span>
<span slot="body">
<table>
<tr><td class="label">Owner</td><td>{owner}</td></tr>
<tr><td class="label">Member</td><td>{owner}</td></tr>
{#if org}
<tr><td class="label">Address</td><td>{org.address}</td></tr>
<tr><td class="label">Safe</td><td>{org.safe}</td></tr>
{/if}
</table>
</span>
<span slot="actions">
<button
on:click={createOrg}
class="primary"
data-waiting={state === State.Waiting || null}
disabled={state !== State.Idle}
>Create</button>
<button on:click={() => dispatch('close')} class="text">
Cancel
</button>
<span slot="actions">
{#if !org}
<button
on:click={createOrg}
class="primary"
data-waiting={[State.Signing, State.Pending].includes(state) || null}
disabled={state !== State.Idle}
>
{#if state === State.Pending}
Creating...
{:else}
Create
{/if}
</button>
<button on:click={() => dispatch('close')} class="text">
Cancel
</button>
{:else}
<button on:click={() => dispatch('close')}>
Done
</button>
{/if}
</span>
</Modal>
+4 -1
View File
@@ -37,6 +37,9 @@ export class Org {
orgFactoryAbi,
config.signer
);
return await orgFactory.createOrg([owner], 1);
return orgFactory.createOrg([owner], 1, {
gasLimit: config.gasLimits.createOrg
});
}
}
+3 -3
View File
@@ -12,19 +12,19 @@
}
export let config;
export let query;
export const query = {};
let modal = null;
let state = State.Idle;
$: owner = $session.address;
$: owner = $session && $session.address;
</script>
<style>
</style>
<main>
<button on:click={() => modal = CreateOrg} disabled={!owner} data-disabled-tooltip="No!" class="secondary">
<button on:click={() => modal = CreateOrg} disabled={!owner} class="secondary">
Create an Org
</button>
</main>
+1 -2
View File
@@ -1,10 +1,9 @@
<script lang="typescript">
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import { Router, Link, Route, navigate } from "svelte-routing";
import { navigate } from "svelte-routing";
import { ethers } from 'ethers';
import { error } from '@app/error';
import { session } from '@app/session';
import { registrar } from './registrar';
import Modal from '@app/Modal.svelte';
+2 -1
View File
@@ -4,6 +4,7 @@
import Begin from '@app/base/register/steps/Begin.svelte';
import Submit from '@app/base/register/steps/Submit.svelte';
export let session;
export let config;
export let query;
</script>
@@ -17,5 +18,5 @@
</Route>
<Route path="register/:name/submit" let:params>
<Submit {config} subdomain={params.name} {query} />
<Submit {config} subdomain={params.name} {query} {session} />
</Route>
+3 -3
View File
@@ -2,7 +2,7 @@
// TODO: Two registration actions with same label
import { ethers } from "ethers";
import { State, state } from './state';
import { approveSpender, updateBalance } from '@app/session';
import * as session from '@app/session';
import { Failure } from '@app/error';
const registrarAbi = [
@@ -51,7 +51,7 @@ async function approveRegistrar(owner, config) {
state.set(State.Approving);
const amount = await registrationFee(config);
await approveSpender(config.registrar.address, amount, config);
await session.approveSpender(config.registrar.address, amount, config);
}
async function commitAndRegister(name, owner, config) {
@@ -81,7 +81,7 @@ async function commit(commitment, fee, minAge, config) {
.catch(e => console.error(e));
await tx.wait(1);
updateBalance(fee.mul(-1));
session.state.updateBalance(fee.mul(-1));
// TODO: Getting "commitment too new"
state.set(State.WaitingToRegister);
+4 -4
View File
@@ -2,8 +2,8 @@
import { navigate } from 'svelte-routing';
import { error } from '@app/error';
import { formatAddress } from '@app/utils';
import { session } from '@app/session';
import { registrar } from '../registrar';
import { session } from '@app/session';
import Connect from '@app/Connect.svelte';
@@ -18,7 +18,7 @@
export let query;
let state = State.Initial;
$: registrationOwner = query.get("owner") || $session.address;
$: registrationOwner = query.get("owner") || ($session && $session.address);
async function begin() {
state = State.CheckingAvailability;
@@ -64,12 +64,12 @@
Back
</button>
{:else}
{#if $session.address}
{#if $session}
<button on:click={begin} class="primary register">
Begin registration &rarr;
</button>
{:else}
<Connect caption="Connect to register" className="primary" />
<Connect caption="Connect to register" className="primary" {config} />
{/if}
<button on:click={() => navigate("/register")} class="text">
+4 -4
View File
@@ -3,17 +3,17 @@
import { get } from 'svelte/store';
import { navigate } from 'svelte-routing';
import { ethers } from 'ethers';
import { session } from '@app/session';
import { registrar, registerName, registrationFee } from '../registrar';
import { State, state } from '../state';
import type { Session } from '@app/session';
export let config;
export let subdomain;
export let query;
export let session: Session;
let error = null;
let s = get(session);
let registrationOwner = query.get("owner") || s.address;
let registrationOwner = query.get("owner") || session.address;
async function getFee(cfg) {
let fee = await registrationFee(cfg);
@@ -87,7 +87,7 @@
</div>
{:else if $state === State.Registered}
<div class="modal-body">
The name <span class="domain">{subdomain}</span> has been successfully registered to {$session.address}.
The name <span class="domain">{subdomain}</span> has been successfully registered to {session.address}.
</div>
<div class="modal-actions">
<button on:click={() => state.set(State.Idle)} class="primary register">
+14 -14
View File
@@ -1,11 +1,12 @@
<script lang="typescript">
import { onMount } from 'svelte';
import { get, derived, writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
import { ethers } from 'ethers';
import { session } from '@app/session';
import { formatAddress } from '@app/utils';
import { State, state } from './state';
import { getInfo, withdrawVested } from './vesting';
import type { Session } from '@app/session';
let input;
@@ -14,24 +15,23 @@
});
export let config;
export let session: Session;
let contractAddress = "";
const info = writable(null);
let info = null;
async function loadContract(config) {
state.set(State.Loading);
info.set(await getInfo(contractAddress, config));
info = await getInfo(contractAddress, config);
state.set(State.Idle);
}
function reset() {
$info = null;
info = null;
state.set(State.Idle);
}
let isBeneficiary = derived([session, info], ([$s, $i]) => {
return $i && ($i.beneficiary === $s.address);
});
$: isBeneficiary = info && session && (info.beneficiary === session.address);
</script>
<style>
@@ -53,25 +53,25 @@
</style>
<main>
{#if $info}
{#if info}
<div class="modal">
<div class="modal-title">
{contractAddress}
</div>
<div class="modal-body">
{#if $state === State.Withdrawn}
Tokens successfully withdrawn to {formatAddress($info.beneficiary)}.
Tokens successfully withdrawn to {formatAddress(info.beneficiary)}.
{:else}
<table>
<tr><td class="label">Beneficiary</td><td>{$info.beneficiary}</td></tr>
<tr><td class="label">Allocation</td><td>{$info.totalVesting} <strong>{$info.symbol}</strong></td></tr>
<tr><td class="label">Withdrawn</td><td>{$info.withdrawn} <strong>{$info.symbol}</strong></td></tr>
<tr><td class="label">Withdrawable</td><td>{$info.withdrawableBalance} <strong>{$info.symbol}</strong></td></tr>
<tr><td class="label">Beneficiary</td><td>{info.beneficiary}</td></tr>
<tr><td class="label">Allocation</td><td>{info.totalVesting} <strong>{info.symbol}</strong></td></tr>
<tr><td class="label">Withdrawn</td><td>{info.withdrawn} <strong>{info.symbol}</strong></td></tr>
<tr><td class="label">Withdrawable</td><td>{info.withdrawableBalance} <strong>{info.symbol}</strong></td></tr>
</table>
{/if}
</div>
<div class="modal-actions">
{#if $isBeneficiary}
{#if isBeneficiary}
{#if $state === State.WithdrawingSign}
<button disabled data-waiting class="primary small">
Waiting for signature...
+2 -2
View File
@@ -1,6 +1,6 @@
import { ethers } from "ethers";
import { formatBalance } from "@app/utils";
import { refreshBalance } from "@app/session";
import * as session from "@app/session";
import { State, state } from "./state";
const abi = [
@@ -30,7 +30,7 @@ export async function withdrawVested(address, config) {
state.set(State.Withdrawing);
await tx.wait();
refreshBalance(config);
session.state.refreshBalance(config);
state.set(State.Withdrawn);
}
+9 -1
View File
@@ -10,10 +10,12 @@ export type Config = {
registrar: { address: string },
radToken: { address: string },
orgFactory: { address: string },
gasLimits: { createOrg: number },
provider: ethers.providers.JsonRpcProvider,
signer: ethers.Signer,
};
// TODO: Move to JSON.
const addresses = {
homestead: {
registrar: {
@@ -34,11 +36,16 @@ const addresses = {
address: "0x59b5eee36f5fa52400A136Fd4630Ee2bF126a4C0",
},
orgFactory: {
address: "0xe30aA5594FFB52B6bF5bbB21eB7e71Ac525bB028",
address: "0xF3D04e874D07d680e8b26332eEae5b9B1c263121",
},
}
};
/// Gas limits for various transactions.
const gasLimits = {
createOrg: 1_000_000,
};
function isMetamaskInstalled(): boolean {
const { ethereum } = window;
return Boolean(ethereum && ethereum.isMetaMask);
@@ -58,6 +65,7 @@ export async function getConfig(): Promise<Config> {
orgFactory: cfg.orgFactory,
provider: provider,
signer: provider.getSigner(),
gasLimits: gasLimits,
};
} else {
throw `Wrong network: ${network.name}`;
+32
View File
@@ -5,3 +5,35 @@ export enum Failure {
}
export const error = writable(null);
export class Unreachable extends Error {
constructor(value?: never) {
if (value) {
super('unreachable value reached: ' + value);
} else {
super('unreachable code reached');
}
}
}
class AssertionError extends Error {
constructor(message?: string) {
if (message) {
super(`assertion failed: ${message}`);
} else {
super(`assertion failed`);
}
}
}
export function assert(value: any, message?: string): asserts value {
if (! value) {
throw new AssertionError(message);
}
}
export function assertEq(actual: any, expected: any, message?: string) {
if (actual !== expected) {
throw new AssertionError(`assertion failed: expected '${expected}', got '${actual}'`);
}
}
+140 -65
View File
@@ -1,6 +1,8 @@
import { get, writable, Writable } from "svelte/store";
import { get, writable, derived, Writable } from "svelte/store";
import { ethers } from "ethers";
import { getConfig } from "./config";
import type { TransactionReceipt, TransactionResponse } from '@ethersproject/providers';
import type { Config } from "@app/config";
import { Unreachable, assert, assertEq } from "@app/error";
export enum Connection {
Disconnected,
@@ -8,18 +10,146 @@ export enum Connection {
Connected
}
export type Session = {
connection: Connection
address?: string
tokenBalance?: any
export type TxState =
{ state: 'signing' }
| { state: 'pending', hash: string }
| { state: 'success', hash: string, blockHash: string, blockNumber: number }
| { state: 'fail', hash: string, blockHash: string, blockNumber: number, error: string }
| null;
export type State =
{ connection: Connection.Disconnected }
| { connection: Connection.Connecting }
| { connection: Connection.Connected, session: Session };
export interface Session {
address: string
tokenBalance: any
tx: TxState
}
export const createState = (initial: State) => {
const store = writable<State>(initial)
return {
subscribe: store.subscribe,
connect: async (config: Config) => {
let state = get(store);
assertEq(state.connection, Connection.Disconnected);
store.set({ connection: Connection.Connecting });
// TODO: This hangs on Brave, if you have to unlock your wallet..
try {
let accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
} catch (e) {
console.error(e);
}
const token = new ethers.Contract(config.radToken.address, tokenAbi, config.provider);
const signer = config.provider.getSigner();
const address = await signer.getAddress();
try {
const tokenBalance = await token.balanceOf(address);
store.set({
connection: Connection.Connected,
session: { address, tokenBalance, tx: null }
});
} catch (e) {
console.error(e);
}
},
updateBalance: n => {
store.update((s) => {
assert(s.connection === Connection.Connected);
s.session.tokenBalance = s.session.tokenBalance.add(n);
return s;
});
},
refreshBalance: async (config) => {
let state = get(store);
assert(state.connection === Connection.Connected);
const addr = state.session.address;
try {
const token = new ethers.Contract(config.radToken.address, tokenAbi, config.provider);
const tokenBalance = await token.balanceOf(addr);
state.session.tokenBalance = tokenBalance;
store.set(state);
} catch (e) {
console.error(e);
}
},
setTxSigning: () => {
store.update(s => {
switch (s.connection) {
case Connection.Connected:
s.session.tx = { state: 'signing' };
return s;
default:
throw new Unreachable();
}
});
},
setTxPending: (tx: TransactionResponse) => {
store.update(s => {
switch (s.connection) {
case Connection.Connected:
s.session.tx = { state: 'pending', hash: tx.hash };
return s;
default:
throw new Unreachable();
}
});
},
setTxConfirmed: (tx: TransactionReceipt) => {
store.update(s => {
switch (s.connection) {
case Connection.Connected:
assert(s.session.tx.state === 'pending');
if (tx.status === 1) {
s.session.tx = {
state: 'success',
hash: s.session.tx.hash,
blockHash: tx.blockHash,
blockNumber: tx.blockNumber
};
} else {
s.session.tx = {
state: 'fail',
hash: s.session.tx.hash,
blockHash: tx.blockHash,
blockNumber: tx.blockNumber,
error: "Failed"
};
}
return s;
default:
throw new Unreachable();
}
});
},
};
};
export const session: Writable<Session> = writable({
connection: Connection.Disconnected,
export const state = createState({ connection: Connection.Disconnected });
export const session = derived(state, s => {
if (s.connection === Connection.Connected) {
return s.session;
}
return null;
});
session.subscribe(s => {
console.log("Session", s);
state.subscribe(s => {
console.log("session.state", s);
});
const tokenAbi = [
@@ -28,35 +158,6 @@ const tokenAbi = [
"function allowance(address, address) view returns (uint256)",
];
export async function connectWallet() {
session.set({ connection: Connection.Connecting });
// TODO: This hangs on Brave, if you have to unlock your wallet..
try {
let accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
} catch (e) {
console.error(e);
}
const config = await getConfig();
const token = new ethers.Contract(config.radToken.address, tokenAbi, config.provider);
const signer = config.provider.getSigner();
let addr = await signer.getAddress();
try {
let tokenBalance = await token.balanceOf(addr);
session.set({
address: addr,
tokenBalance: tokenBalance,
connection: Connection.Connected
});
} catch (e) {
console.error(e);
}
}
export async function approveSpender(spender, amount, config) {
const token = new ethers.Contract(config.radToken.address, tokenAbi, config.provider);
const signer = config.provider.getSigner();
@@ -70,32 +171,6 @@ export async function approveSpender(spender, amount, config) {
}
}
export async function updateBalance(n) {
session.update((s) => {
s.tokenBalance = s.tokenBalance.add(n);
return s;
});
}
export async function refreshBalance(config) {
const addr = get(session).address;
if (addr) {
try {
const token = new ethers.Contract(config.radToken.address, tokenAbi, config.provider);
const tokenBalance = await token.balanceOf(addr);
console.log("new balance", tokenBalance);
session.update((s) => {
s.tokenBalance = tokenBalance;
return s;
});
} catch (e) {
console.error(e);
}
}
}
export function disconnectWallet() {
location.reload();
}