Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Improve session code
Alexis Sellier committed 5 years ago
commit 10e2847d5d74061b7ebfa90000abe41671068ff2
parent 451ae1fdc35f18ebc46e8c01b80dde1380af9ba1
17 files changed +283 -133
modified src/App.svelte
@@ -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>
modified src/Connect.svelte
@@ -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}
modified src/Header.svelte
@@ -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>
modified src/Modal.svelte
@@ -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
modified src/base/orgs/CreateOrg.svelte
@@ -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>
+
    {#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>
modified src/base/orgs/Org.ts
@@ -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
+
    });
  }
}
modified src/base/orgs/Orgs.svelte
@@ -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>
modified src/base/register/Register.svelte
@@ -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';
modified src/base/register/Routes.svelte
@@ -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>
modified src/base/register/registrar.ts
@@ -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);
modified src/base/register/steps/Begin.svelte
@@ -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">
modified src/base/register/steps/Submit.svelte
@@ -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">
modified src/base/vesting/Vesting.svelte
@@ -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...
modified src/base/vesting/vesting.ts
@@ -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);
}

modified src/config.ts
@@ -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}`;
modified src/error.ts
@@ -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}'`);
+
  }
+
}
modified src/session.ts
@@ -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();
}