Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Refactor app
Alexis Sellier committed 5 years ago
commit e05b1356d8fcc70775393c37a70a41469a346106
parent 869a531cdf977b2948f303f5eadabd40c06129d9
14 files changed +385 -191
modified public/index.css
@@ -44,6 +44,7 @@
	--font-family-sans-serif: Inter;
	--font-family-monospace: monospace;
	--border-radius: 50px;
+
	--box-shadow-color: var(--color-secondary-2);
}


@@ -91,7 +92,7 @@ button {
	min-width: 8rem;
}
button:hover {
-
	color: var(--color-background);
+
	color: var(--color-background) !important;
	background-color: var(--color-foreground);
}
button.waiting {
@@ -101,12 +102,14 @@ button.secondary {
	color: var(--color-secondary);
	border-color: var(--color-secondary);
}
+
button.secondary:hover {
+
	background-color: var(--color-secondary);
+
}
button.primary {
	color: var(--color-primary);
	border-color: var(--color-primary);
}
button.primary:hover {
-
	color: var(--color-background);
	background-color: var(--color-primary);
}
button.primary[disabled] {
@@ -128,7 +131,6 @@ button.text {
	min-width: 3rem;
}
button.text:hover {
-
	color: var(--color-background);
	background-color: var(--color-foreground);
}

@@ -170,18 +172,21 @@ input.wide {
.modal {
	padding: 2rem;
	border: 1px solid var(--color-secondary);
-
	box-shadow: 8px 8px 64px var(--color-secondary-2);
+
	background: var(--color-background);
+
	box-shadow: 8px 8px 64px var(--box-shadow-color);
	min-width: 480px;
+
	max-width: 720px;
	text-align: center;
}
.modal-title {
	font-size: 1.75rem;
-
	margin-bottom: 1rem;
+
	margin-bottom: 2rem;
	color: var(--color-secondary);
	text-align: center;
}
.modal-body {
-
	margin: 3rem 0;
+
	margin-top: 2em;
+
	margin-bottom: 3em;
}
.modal-actions {
	margin-top: 2rem;
@@ -194,6 +199,19 @@ input.wide {
	margin-right: 0;
}

+
.error {
+
	color: var(--color-negative);
+
	border-color: var(--color-negative);
+
	--box-shadow-color: var(--color-negative-2);
+
}
+
.highlight {
+
	color: var(--color-secondary);
+
}
+

+
button.error:hover {
+
	background-color: var(--color-negative);
+
}
+

table {
	table-layout: fixed;
	border-collapse: collapse;
modified src/App.svelte
@@ -7,11 +7,12 @@
  import { session } from '@app/session';

  import Vesting from '@app/base/vesting/Vesting.svelte';
-
  import Register from '@app/base/register/Register.svelte';
+
  import Register from '@app/base/register/Routes.svelte';
  import Header from '@app/Header.svelte';

  const defaultPath = "register";
  const path = window.location.pathname;
+
  const query = new URLSearchParams(window.location.search);
  export let url = path === "/" ? defaultPath : path;

  function handleKeydown(event) {
@@ -42,9 +43,7 @@
        <Route path="vesting">
          <Vesting {config} />
        </Route>
-
        <Route path="register">
-
          <Register {config} />
-
        </Route>
+
        <Register {config} {query} />
      </Router>
    </div>
  </div>
modified src/Header.svelte
@@ -4,9 +4,9 @@
  import { derived } from "svelte/store";
  import { ethers } from "ethers";
  import { link } from "svelte-routing";
-
  import { formatBalance } from "@app/utils";
+
  import { formatBalance, formatAddress } from "@app/utils";
  import { error, Failure } from '@app/error';
-
  import { session, disconnectWallet, shortAddress } from "@app/session";
+
  import { session, disconnectWallet } from "@app/session";
  import Logo from './Logo.svelte';
  import Connect from './Connect.svelte';

@@ -43,9 +43,8 @@
  .error {
    text-align: center;
    color: var(--color-negative);
-
    background-color: var(--color-negative-2);
+
    border: 1px solid var(--color-negative);
    padding: 0.5rem;
-
    border-radius: var(--border-radius);
  }
  .error a {
    color: var(--color-negative);
@@ -64,7 +63,11 @@
{#if $error}
  {#if $error.type === Failure.TransactionFailed}
    <div class="error">
-
      <strong>Error:</strong> Transaction <a href="https://etherscan.io/tx/{$error.hash}">{$error.hash}</a> failed.
+
      {#if $error.message}
+
        <strong>Error:</strong> {$error.message}
+
      {:else if $error.txHash}
+
        <strong>Error:</strong> Transaction <a href="https://etherscan.io/tx/{$error.txHash}">{$error.txHash}</a> failed.
+
      {/if}
    </div>
  {/if}
{/if}
@@ -91,7 +94,7 @@
        {#if sessionButtonHover}
          Disconnect
        {:else}
-
          {shortAddress($address)}
+
          {formatAddress($address)}
        {/if}
      </button>
    {:else}
added src/Modal.svelte
@@ -0,0 +1,44 @@
+
<script lang="typescript">
+
  export let floating = false;
+

+
  let className = floating ? "modal-floating" : "";
+
</script>
+

+
<style>
+
  .modal-floating, .modal-overlay {
+
    position: fixed;
+
    top: 0;
+
    left: 0;
+
    width: 100%;
+
    height: 100%;
+
    overflow: hidden;
+
  }
+
  .modal-floating {
+
    z-index: 300;
+
    display: flex;
+
    align-items: center;
+
    justify-content: center;
+
  }
+
  .modal-overlay {
+
    z-index: 200;
+
    background-color: rgba(0, 0, 0, .75);
+
  }
+
</style>
+

+
{#if floating}
+
  <div class="modal-overlay"></div>
+
{/if}
+

+
<div class={className}>
+
  <div class="modal">
+
    <div class="modal-title">
+
      <slot name="title"></slot>
+
    </div>
+
    <div class="modal-body">
+
      <slot name="body"></slot>
+
    </div>
+
    <div class="modal-actions">
+
      <slot name="actions"></slot>
+
    </div>
+
  </div>
+
</div>
modified src/base/register/Register.svelte
@@ -1,26 +1,42 @@
<script lang="typescript">
  import { onMount } from 'svelte';
  import { get } from 'svelte/store';
+
  import { Router, Link, Route, navigate } from "svelte-routing";
  import { ethers } from 'ethers';
  import { error } from '@app/error';
  import { session } from '@app/session';
-
  import { registrar, registerName, registrationFee } from './registrar';
-
  import { State, state } from './state';
+
  import { registrar } from './registrar';

-
  import RegisterButton from './RegisterButton.svelte';
+
  import Modal from '@app/Modal.svelte';
+

+
  enum State {
+
    Idle,
+
    CheckingAvailability,
+
    NameAvailable,
+
    NameUnavailable,
+
  }

  export let config;

-
  let subdomain = "";
-
  let input;
+
  let state = State.Idle;
+
  let inputValue;
+
  let inputElement;

  onMount(() => {
-
    input.focus();
+
    inputElement.focus();
  });

-
  async function getFee(cfg) {
-
    let fee = await registrationFee(cfg);
-
    return ethers.utils.formatUnits(fee);
+
  function checkAvailability(name) {
+
    state = State.CheckingAvailability;
+

+
    registrar(config).available(name).then(isAvailable => {
+
      if (isAvailable) {
+
        state = State.NameAvailable;
+
        navigate(`/register/${name}`);
+
      } else {
+
        state = State.NameUnavailable;
+
      }
+
    });
  }
</script>

@@ -39,10 +55,6 @@
  input.subdomain[disabled] {
    color: var(--color-secondary);
  }
-
  .available {
-
    line-height: 1.75em;
-
    padding: 2rem;
-
  }
  div.input-caption {
    font-size: 1.25rem;
    text-align: left;
@@ -57,9 +69,6 @@
    margin-left: 1.5rem;
    color: var(--color-secondary);
  }
-
  .domain {
-
    color: var(--color-secondary);
-
  }
  .name {
    margin: 1rem;
  }
@@ -86,49 +95,51 @@
</style>

<main>
-
  {#if $state === State.Idle  || $state === State.CheckingAvailability}
-
    <div class="input-caption">
-
      Register a <strong>radicle.eth</strong> name
-
    </div>
-
    <div class="input-main">
-
      <span class="name">
-
        <div>
-
          <input
-
            bind:this={input}
-
            bind:value={subdomain}
-
            placeholder=""
-
            class="subdomain"
-
            disabled={$state === State.CheckingAvailability}
-
            type="text"
-
          />
-
          <span class="root">.radicle.eth</span>
-
        </div>
+
  {#if state === State.NameUnavailable}
+
    <Modal floating>
+
      <span slot="title">
+
        {inputValue}.radicle.eth
      </span>
-
      <RegisterButton {subdomain} {config} />
-
    </div>
-
  {:else}
-
    <div class="modal">
-
      <div class="modal-title">
-
        {subdomain}.radicle.eth
-
      </div>
-
      {#if $state === State.Registered}
-
        <div class="available">The name <span class="domain">{subdomain}</span> has been successfully registered to {$session.address}.</div>
-
      {:else if $state === State.NameAvailable}
-
        <div class="available">The name <span class="domain">{subdomain}</span> is available for registration.</div>
-
      {:else if $state === State.Approving}
-
        <div class="available">
-
          Approving Radicle for {#await getFee(config)}
-
            ?
-
          {:then fee}
-
            {fee}
-
          {/await} <strong>RAD</strong>...
-
        </div>
-
      {:else if $state == State.NameUnavailable}
-
        <div class="available">The name <span class="domain">{subdomain}</span> is not available for registration.</div>
-
      {/if}
-
      <div class="modal-actions">
-
        <RegisterButton {subdomain} {config} />
-
      </div>
-
    </div>
+
      <span slot="body">
+
        The name <span class="highlight">{inputValue}</span> is not available for registration.
+
      </span>
+
      <span slot="actions">
+
        <button on:click={() => state = State.Idle} class="secondary">
+
          Back
+
        </button>
+
      </span>
+
    </Modal>
  {/if}
+

+
  <div class="input-caption">
+
    Register a <strong>radicle.eth</strong> name
+
  </div>
+
  <div class="input-main">
+
    <span class="name">
+
      <div>
+
        <input
+
          bind:this={inputElement}
+
          bind:value={inputValue}
+
          placeholder=""
+
          class="subdomain"
+
          disabled={state === State.CheckingAvailability}
+
          type="text"
+
        />
+
        <span class="root">.radicle.eth</span>
+
      </div>
+
    </span>
+
    {#if inputValue === ""}
+
      <button disabled class="primary register">
+
        Check
+
      </button>
+
    {:else if state === State.CheckingAvailability}
+
      <button disabled class="primary register" data-waiting>
+
        Check
+
      </button>
+
    {:else}
+
      <button on:click={() => checkAvailability(inputValue)} class="primary register">
+
        Check
+
      </button>
+
    {/if}
+
  </div>
</main>
deleted src/base/register/RegisterButton.svelte
@@ -1,91 +0,0 @@
-
<script lang="typescript">
-
  import { get } from 'svelte/store';
-
  import { error } from '@app/error';
-
  import { session } from '@app/session';
-
  import { State, state } from './state';
-
  import { registrar, registerName } from './registrar';
-

-
  import Connect from '@app/Connect.svelte';
-

-
  export let subdomain;
-
  export let config;
-

-
  async function register() {
-
    let sess = get(session);
-
    let oldState = get(state);
-

-
    try {
-
      await registerName(subdomain, sess.address, config);
-
    } catch (e) {
-
      console.error(e);
-

-
      state.set(oldState);
-
      error.set(e);
-
    }
-
  }
-

-
  async function checkAvailability() {
-
    registrar(config).available(subdomain).then(isAvailable => {
-
      if (isAvailable) {
-
        state.set(State.NameAvailable);
-
      } else {
-
        state.set(State.NameUnavailable);
-
      }
-
    });
-
    state.set(State.CheckingAvailability);
-
  }
-

-
  function cancel() {
-
    state.set(State.Idle);
-
    error.set(null);
-
  }
-
</script>
-

-
<style>
-
  .cancel {
-
    margin-left: 1rem;
-
  }
-
</style>
-

-
{#if $state >= State.NameAvailable && $state < State.Registered}
-
  {#if $session.address}
-
    <button on:click={register} disabled={$state > State.NameAvailable} class="primary register">
-
      {#if $state === State.Approving}
-
        Approving...
-
      {:else if $state === State.Committing}
-
        Committing...
-
      {:else if $state === State.WaitingToRegister}
-
        Waiting...
-
      {:else if $state === State.Registering}
-
        Registering...
-
      {:else}
-
        Begin registration &rarr;
-
      {/if}
-
    </button>
-
  {:else}
-
    <Connect caption="Connect to register" className="primary" />
-
  {/if}
-
  <button on:click={cancel} class="cancel text">
-
    Cancel
-
  </button>
-
{:else if $state === State.Registered}
-
  <button on:click={() => state.set(State.Idle)}>
-
    Done
-
  </button>
-
{:else if $state === State.NameUnavailable}
-
  <button on:click={() => state.set(State.Idle)}>
-
    Back
-
  </button>
-
{:else if subdomain == ""}
-
  <button disabled class="primary register">
-
    Check
-
  </button>
-
{:else if $state === State.CheckingAvailability}
-
  <button disabled class="primary register" data-waiting>
-
    Check
-
  </button>
-
{:else}
-
  <button on:click={checkAvailability} class="primary register">
-
    Check
-
  </button>
-
{/if}
added src/base/register/Routes.svelte
@@ -0,0 +1,21 @@
+
<script lang="typescript">
+
  import { Route } from "svelte-routing";
+
  import Register from '@app/base/register/Register.svelte';
+
  import Begin from '@app/base/register/steps/Begin.svelte';
+
  import Submit from '@app/base/register/steps/Submit.svelte';
+

+
  export let config;
+
  export let query;
+
</script>
+

+
<Route path="register">
+
  <Register {config} />
+
</Route>
+

+
<Route path="register/:name" let:params>
+
  <Begin {config} subdomain={params.name} {query} />
+
</Route>
+

+
<Route path="register/:name/submit" let:params>
+
  <Submit {config} subdomain={params.name} {query} />
+
</Route>
modified src/base/register/registrar.ts
@@ -34,12 +34,16 @@ export async function registerName(name, owner, config) {
  let commitmentJson = window.localStorage.getItem('commitment');
  let commitment = commitmentJson && JSON.parse(commitmentJson);

-
  // Try to recover an existing commitment.
-
  if (commitment && commitment.name === name && commitment.owner === owner) {
-
    await register(name, owner, commitment.salt, config);
-
  } else {
-
    await approveRegistrar(owner, config);
-
    await commitAndRegister(name, owner, config);
+
  try {
+
    // Try to recover an existing commitment.
+
    if (commitment && commitment.name === name && commitment.owner === owner) {
+
      await register(name, owner, commitment.salt, config);
+
    } else {
+
      await approveRegistrar(owner, config);
+
      await commitAndRegister(name, owner, config);
+
    }
+
  } catch (e) {
+
    throw { type: Failure.TransactionFailed, message: e.message, hash: e.txHash };
  }
}

@@ -93,13 +97,9 @@ async function register(name, owner, salt, config) {
  );
  console.log("Sent", tx);

-
  try {
-
    await tx.wait();
-
    window.localStorage.clear();
-
    state.set(State.Registered);
-
  } catch (e) {
-
    throw { type: Failure.TransactionFailed, hash: tx.hash };
-
  }
+
  await tx.wait();
+
  window.localStorage.clear();
+
  state.set(State.Registered);
}

function makeCommitment(name, owner, salt) {
modified src/base/register/state.ts
@@ -1,11 +1,8 @@
import { derived, writable } from "svelte/store";

export enum State {
-
  Error = -1,
+
  Failed = -1,
  Idle,
-
  CheckingAvailability,
-
  NameUnavailable,
-
  NameAvailable,
  Approving,
  Committing,
  WaitingToRegister,
added src/base/register/steps/Begin.svelte
@@ -0,0 +1,80 @@
+
<script lang="typescript">
+
  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 Connect from '@app/Connect.svelte';
+

+
  enum State {
+
    Initial,
+
    CheckingAvailability,
+
    NameUnavailable,
+
  }
+

+
  export let config;
+
  export let subdomain;
+
  export let query;
+

+
  let state = State.Initial;
+
  $: registrationOwner = query.get("owner") || $session.address;
+

+
  async function begin() {
+
    state = State.CheckingAvailability;
+

+
    if (await registrar(config).available(subdomain)) {
+
      navigate(`/register/${subdomain}/submit?${
+
        registrationOwner ? new URLSearchParams({ owner: registrationOwner }) : ''
+
      }`);
+
    } else {
+
      state = State.NameUnavailable;
+
    }
+
  }
+
</script>
+

+
<style>
+
</style>
+

+
<div class="modal">
+
  <div class="modal-title">
+
    {subdomain}.radicle.eth
+
  </div>
+

+
  <div class="modal-body">
+
    {#if state === State.Initial || state === State.CheckingAvailability}
+
      {#if registrationOwner}
+
        The name <span class="highlight">{subdomain}</span> is available for registration
+
        under account <span class="highlight">{formatAddress(registrationOwner)}</span>.
+
      {:else}
+
        The name <span class="highlight">{subdomain}</span> is available for registration.
+
      {/if}
+
    {:else if state === State.NameUnavailable}
+
      The name <span class="highlight">{subdomain}</span> is not available for registration.
+
    {/if}
+
  </div>
+

+
  <div class="modal-actions">
+
    {#if state === State.CheckingAvailability}
+
      <button disabled class="primary register">
+
        Checking availability...
+
      </button>
+
    {:else if state === State.NameUnavailable}
+
      <button on:click={() => navigate("/register")} class="">
+
        Back
+
      </button>
+
    {:else}
+
      {#if $session.address}
+
        <button on:click={begin} class="primary register">
+
          Begin registration &rarr;
+
        </button>
+
      {:else}
+
        <Connect caption="Connect to register" className="primary" />
+
      {/if}
+

+
      <button on:click={() => navigate("/register")} class="text">
+
        Cancel
+
      </button>
+
    {/if}
+
  </div>
+
</div>
added src/base/register/steps/Submit.svelte
@@ -0,0 +1,111 @@
+
<script lang="typescript">
+
  import { onMount } from 'svelte';
+
  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';
+

+
  export let config;
+
  export let subdomain;
+
  export let query;
+

+
  let error = null;
+
  let s = get(session);
+
  let registrationOwner = query.get("owner") || s.address;
+

+
  async function getFee(cfg) {
+
    let fee = await registrationFee(cfg);
+
    return ethers.utils.formatUnits(fee);
+
  }
+

+
  onMount(async () => {
+
    let oldState = get(state);
+

+
    try {
+
      await registerName(subdomain, registrationOwner, config);
+
    } catch (e) {
+
      console.error("Error", e);
+

+
      state.set(oldState);
+
      error = e;
+
    }
+
  });
+
</script>
+

+
<style>
+
  .available {
+
    line-height: 1.75em;
+
    padding: 2rem;
+
  }
+
  .domain {
+
    color: var(--color-secondary);
+
  }
+
</style>
+

+
<div class="modal {error ? 'error' : ''}">
+
  {#if error}
+
    <div class="modal-title error">
+
      Transaction failed
+
    </div>
+
  {:else}
+
    <div class="modal-title">
+
      {subdomain}.radicle.eth
+
    </div>
+
  {/if}
+

+
  {#if $state === State.Approving}
+
    <div class="modal-body">
+
      Approving Registry for {#await getFee(config)}
+
        ?
+
      {:then fee}
+
        {fee}
+
      {/await} <strong>RAD</strong>...
+
    </div>
+
    <div class="modal-actions">
+
      <button disabled class="primary register">
+
        Approving...
+
      </button>
+
    </div>
+
  {:else if $state === State.Committing}
+
    <div class="modal-actions">
+
      <button disabled class="primary register">
+
        Committing...
+
      </button>
+
    </div>
+
  {:else if $state === State.WaitingToRegister}
+
    <div class="modal-body">
+
      Waiting for wallet confirmation...
+
    </div>
+
    <div class="modal-actions">
+
      <button disabled class="primary register">
+
        Waiting...
+
      </button>
+
    </div>
+
  {:else if $state === State.Registering}
+
    <div class="modal-actions">
+
      <button disabled class="primary register">
+
        Registering...
+
      </button>
+
    </div>
+
  {:else if $state === State.Registered}
+
    <div class="modal-body">
+
      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">
+
        Done
+
      </button>
+
    </div>
+
  {:else if error}
+
    <div class="modal-body error">
+
      <strong>Error:</strong> {error.message}
+
    </div>
+
    <div class="modal-actions">
+
      <button on:click={() => navigate("/register")} class="error">
+
        Back
+
      </button>
+
    </div>
+
  {/if}
+
</div>
modified src/base/vesting/Vesting.svelte
@@ -2,7 +2,8 @@
  import { onMount } from 'svelte';
  import { get, derived, writable } from 'svelte/store';
  import { ethers } from 'ethers';
-
  import { session, shortAddress } from '@app/session';
+
  import { session } from '@app/session';
+
  import { formatAddress } from '@app/utils';
  import { State, state } from './state';
  import { getInfo, withdrawVested } from './vesting';

@@ -59,7 +60,7 @@
      </div>
      <div class="modal-body">
        {#if $state === State.Withdrawn}
-
          Tokens successfully withdrawn to {shortAddress($info.beneficiary)}.
+
          Tokens successfully withdrawn to {formatAddress($info.beneficiary)}.
        {:else}
          <table>
            <tr><td class="label">Beneficiary</td><td>{$info.beneficiary}</td></tr>
modified src/session.ts
@@ -99,9 +99,3 @@ export async function refreshBalance(config) {
export function disconnectWallet() {
  location.reload();
}
-

-
export function shortAddress(addr) {
-
  return addr.substring(0, 6)
-
    + '...'
-
    + addr.substring(addr.length - 4, addr.length);
-
}
modified src/utils.ts
@@ -3,3 +3,9 @@ import { ethers } from "ethers";
export function formatBalance(n) {
  return ethers.utils.commify(parseFloat(ethers.utils.formatUnits(n)).toFixed(2));
}
+

+
export function formatAddress(addr) {
+
  return addr.substring(0, 6)
+
    + '...'
+
    + addr.substring(addr.length - 4, addr.length);
+
}