Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add "Register" module
Alexis Sellier committed 5 years ago
commit 9eded4d24bf5f0035928afd007b8f44446819724
parent 7a313f640c6fad6f771b3a1c4e472ad537f31453
6 files changed +412 -58
modified src/App.svelte
@@ -6,6 +6,7 @@
  import { session } from './session.js';

  import Vesting from './base/vesting/Vesting.svelte';
+
  import Register from './base/register/Register.svelte';
  import Header from './Header.svelte';

  export let url = "";
@@ -35,11 +36,22 @@
</style>

<svelte:window on:keydown={handleKeydown} />
-
<div class="app">
-
  <Header/>
-
  <div class="wrapper">
-
    <Router url="{url}">
-
      <Route path="vesting" component={Vesting} />
-
    </Router>
+
{#await getConfig()}
+
  <!-- Loading wallet -->
+
{:then config}
+
  <div class="app">
+
    <Header/>
+
    <div class="wrapper">
+
      <Router url="{url}">
+
        <Route path="vesting">
+
          <Vesting {config} />
+
        </Route>
+
        <Route path="register">
+
          <Register {config} />
+
        </Route>
+
      </Router>
+
    </div>
  </div>
-
</div>
+
{:catch err}
+
  <!-- Show error -->
+
{/await}
added src/base/register/Register.svelte
@@ -0,0 +1,129 @@
+
<script lang="javascript">
+
  import { ethers } from 'ethers';
+
  import { get } from 'svelte/store';
+
  import { STATE, state } from './state.js';
+
  import { error } from '../../error.js';
+
  import { session } from '../../session.js';
+
  import { registrar, registerName, registrationFee } from './registrar.js';
+
  import RegisterButton from './RegisterButton.svelte';
+

+
  export let config = null;
+

+
  let subdomain = "";
+

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

+
<style>
+
  main {
+
    padding-top: 2rem;
+
    align-self: center;
+
  }
+
  input.subdomain {
+
    margin-right: 0;
+
    border-radius-top-right: 0;
+
    border-radius-bottom-right: 0;
+
    border-radius: var(--border-radius) 0 0 var(--border-radius);
+
    border-right: none;
+
  }
+
  input.subdomain[disabled] {
+
    color: var(--color-secondary);
+
  }
+
  .available {
+
    line-height: 1.75em;
+
    padding: 2rem;
+
  }
+
  div.input-caption {
+
    font-size: 1.25rem;
+
    text-align: left;
+
    margin-left: 1.5rem;
+
    padding-left: 1.5rem;
+
    color: var(--color-secondary);
+
  }
+
  div.input-main {
+
    display: flex;
+
    align-items: center;
+
    flex-direction: row;
+
    margin-left: 1.5rem;
+
    color: var(--color-secondary);
+
  }
+
  .domain {
+
    color: var(--color-secondary);
+
  }
+
  .name {
+
    margin: 1rem;
+
  }
+
  .name div {
+
    display: flex;
+
    flex-direction: row;
+
    align-items: center;
+
    justify-content: center;
+
  }
+
  .name input {
+
    margin: 0;
+
  }
+
  span.root {
+
    line-height: 1.5em;
+
    margin: 1rem;
+
    margin-left: 0;
+
    margin-right: 0;
+
	padding: 1rem 2rem;
+
    color: var(--color-secondary);
+
    border-radius: 0 var(--border-radius) var(--border-radius) 0;
+
    border: 1px solid var(--color-secondary);
+
    border-left: none;
+
  }
+
  .register {
+
    margin: 1rem;
+
  }
+
</style>
+

+
<main>
+
  {#if $state === STATE.IDLE  || $state === STATE.CHECKING_AVAILABILITY}
+
    <div class="input-caption">
+
      Register a <strong>radicle.eth</strong> name
+
    </div>
+
    <div class="input-main">
+
      <span class="name">
+
        <div>
+
          <input
+
            autofocus
+
            placeholder=""
+
            class="subdomain"
+
            disabled={$state === STATE.CHECKING_AVAILABILITY}
+
            type="text" bind:value={subdomain}
+
          />
+
          <span class="root">.radicle.eth</span>
+
        </div>
+
      </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.NAME_AVAILABLE}
+
        <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.NAME_UNAVAILABLE}
+
        <div class="available">The name <span class="domain">{subdomain}</span> is not available for registration.</div>
+
      {/if}
+
      <div class="modal-actions">
+
        <RegisterButton {subdomain} />
+
      </div>
+
    </div>
+
  {/if}
+
</main>
added src/base/register/RegisterButton.svelte
@@ -0,0 +1,90 @@
+
<script lang="javascript">
+
  import {get} from 'svelte/store';
+
  import {STATE, state} from './state.js';
+
  import {registrar, registerName} from './registrar.js';
+
  import {error} from '../../error.js';
+
  import {session} from '../../session.js';
+
  import Connect from '../../Connect.svelte';
+

+
  export let subdomain = "";
+
  export let config = null;
+

+
  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.NAME_AVAILABLE);
+
      } else {
+
        state.set(STATE.NAME_UNAVAILABLE);
+
      }
+
    });
+
    state.set(STATE.CHECKING_AVAILABILITY);
+
  }
+

+
  function cancel() {
+
    state.set(STATE.IDLE);
+
    error.set(null);
+
  }
+
</script>
+

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

+
{#if $state >= STATE.NAME_AVAILABLE && $state < STATE.REGISTERED}
+
  {#if $session.address}
+
    <button on:click={register} disabled={$state > STATE.NAME_AVAILABLE} class="primary register">
+
      {#if $state === STATE.APPROVING}
+
        Approving...
+
      {:else if $state === STATE.COMMITTING}
+
        Committing...
+
      {:else if $state === STATE.WAITING_TO_REGISTER}
+
        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.NAME_UNAVAILABLE}
+
  <button on:click={() => state.set(STATE.IDLE)}>
+
    Back
+
  </button>
+
{:else if subdomain == ""}
+
  <button disabled class="primary register">
+
    Check
+
  </button>
+
{:else if $state === STATE.CHECKING_AVAILABILITY}
+
  <button disabled class="primary register" data-waiting>
+
    Check
+
  </button>
+
{:else}
+
  <button on:click={checkAvailability} class="primary register">
+
    Check
+
  </button>
+
{/if}
added src/base/register/registrar.js
@@ -0,0 +1,112 @@
+
// TODO: Show "look at your wallet" / "confirm tx" before state change.
+
// TODO: Two registration actions with same label
+
import { ethers } from "ethers";
+
import { STATE, state } from './state.js';
+
import { approveSpender, updateBalance } from '../../session.js';
+
import { ERROR } from '../../error.js';
+

+
const registrarAbi = [
+
  {"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"commitment","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"blockNumber","type":"uint256"}],"name":"CommitmentMade","type":"event"},
+
  {"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},
+
  {"inputs":[{"internalType":"string","name":"name","type":"string"}],"name":"available","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},
+
  {"inputs":[{"internalType":"bytes32","name":"commitment","type":"bytes32"}],"name":"commit","outputs":[],"stateMutability":"nonpayable","type":"function"},
+
  {"inputs":[],"name":"commitments","outputs":[{"internalType":"contract Commitments","name":"","type":"address"}],"stateMutability":"view","type":"function"},
+
  {"inputs":[],"name":"ens","outputs":[{"internalType":"contract ENS","name":"","type":"address"}],"stateMutability":"view","type":"function"},
+
  {"inputs":[],"name":"minCommitmentAge","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
+
  {"inputs":[{"internalType":"bytes32","name":"parent","type":"bytes32"},{"internalType":"bytes32","name":"label","type":"bytes32"}],"name":"namehash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"pure","type":"function"},
+
  {"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
+
  {"inputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"salt","type":"uint256"}],"name":"register","outputs":[],"stateMutability":"nonpayable","type":"function"},
+
  {"inputs":[],"name":"registrationFeeRad","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
+
  {"inputs":[{"internalType":"string","name":"name","type":"string"}],"name":"valid","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"}
+
];
+

+
export function registrar(config) {
+
  return new ethers.Contract(config.registrar.address, registrarAbi, config.provider);
+
}
+

+
export async function registrationFee(config) {
+
  return await registrar(config).registrationFeeRad();
+
}
+

+
export async function registerName(name, owner, config) {
+
  if (! name) return;
+

+
  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);
+
  }
+
}
+

+
async function approveRegistrar(owner, config) {
+
  state.set(STATE.APPROVING);
+

+
  const amount = await registrationFee(config);
+
  await approveSpender(config.registrar.address, amount, config);
+
}
+

+
async function commitAndRegister(name, owner, config) {
+
  let salt = ethers.utils.randomBytes(32);
+
  let minAge = (await registrar(config).minCommitmentAge()).toNumber();
+
  let fee = await registrationFee(config);
+

+
  await commit(makeCommitment(name, owner, salt), fee, minAge, config);
+
  // Save commitment in local storage in case the user refreshes or closes
+
  // the page.
+
  window.localStorage.setItem('commitment', JSON.stringify({
+
    name: name,
+
    owner: owner,
+
    salt: ethers.utils.hexlify(salt)
+
  }));
+

+
  await register(name, owner, salt, config);
+
}
+

+
async function commit(commitment, fee, minAge, config) {
+
  state.set(STATE.COMMITTING);
+

+
  const signer = config.provider.getSigner();
+
  const tx = await registrar(config)
+
    .connect(signer)
+
    .commit(commitment, { gasLimit: 150000 })
+
    .catch(e => console.error(e));
+

+
  await tx.wait(1);
+
  updateBalance(fee.mul(-1));
+

+
  // TODO: Getting "commitment too new"
+
  state.set(STATE.WAITING_TO_REGISTER);
+
  await tx.wait(minAge + 1);
+
}
+

+
async function register(name, owner, salt, config) {
+
  state.set(STATE.REGISTERING);
+

+
  const signer = config.provider.getSigner();
+
  const tx = await registrar(config).connect(signer).register(
+
    name, owner, ethers.BigNumber.from(salt), { gasLimit: 150000 }
+
  );
+
  console.log("Sent", tx);
+

+
  try {
+
    await tx.wait();
+
    window.localStorage.clear();
+
    state.set(STATE.REGISTERED);
+
  } catch (e) {
+
    throw { type: ERROR.TRANSACTION_FAILED, hash: tx.hash };
+
  }
+
}
+

+
function makeCommitment(name, owner, salt) {
+
  let bytes = ethers.utils.concat([
+
    ethers.utils.toUtf8Bytes(name),
+
    ethers.utils.getAddress(owner),
+
    ethers.BigNumber.from(salt),
+
  ]);
+
  return ethers.utils.keccak256(bytes);
+
}
added src/base/register/state.js
@@ -0,0 +1,16 @@
+
import { derived, writable } from "svelte/store";
+

+
export const STATE = {
+
  ERROR: -1,
+
  IDLE: 0,
+
  CHECKING_AVAILABILITY: 1,
+
  NAME_UNAVAILABLE: 2,
+
  NAME_AVAILABLE: 3,
+
  APPROVING: 4,
+
  COMMITTING: 5,
+
  WAITING_TO_REGISTER: 6,
+
  REGISTERING: 7,
+
  REGISTERED: 8,
+
};
+

+
export const state = writable(STATE.IDLE);
modified src/base/vesting/Vesting.svelte
@@ -2,9 +2,10 @@
  import { ethers } from 'ethers';
  import { get } from 'svelte/store';
  import { STATE, state } from './state.js';
-
  import { getConfig } from '../../config.js';
  import { getInfo } from './vesting.js';

+
  export let config = null;
+

  let contractAddress = "";
  let info = null;

@@ -33,57 +34,51 @@
  }
</style>

-
{#await getConfig()}
-
  <!-- Loading wallet -->
-
{:then config}
-
  <main>
-
    {#if info}
-
      <div class="modal">
-
        <div class="modal-title">
-
          {contractAddress}
-
        </div>
-
        <div class="modal-body">
-
          <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>
-
          </table>
-
        </div>
-
        <div class="modal-actions">
-
          <button on:click={() => info = null} class="small">
-
            Back
-
          </button>
-
        </div>
+
<main>
+
  {#if info}
+
    <div class="modal">
+
      <div class="modal-title">
+
        {contractAddress}
      </div>
-
    {:else}
-
      <div class="input-caption">
-
        Enter your Radicle <strong>vesting contract</strong> address
+
      <div class="modal-body">
+
        <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>
+
        </table>
      </div>
-
      <div class="input-main">
-
        <span class="name">
-
          <div>
-
            <input
-
              autofocus
-
              size="40"
-
              placeholder=""
-
              class="subdomain"
-
              disabled={$state === STATE.LOADING}
-
              type="text" bind:value={contractAddress}
-
            />
-
          </div>
-
        </span>
-
        <button
-
          on:click={() => loadContract(config)}
-
          class="primary"
-
          data-waiting={$state === STATE.LOADING || null}
-
          disabled={$state === STATE.LOADING}
-
        >
-
          Load
+
      <div class="modal-actions">
+
        <button on:click={() => info = null} class="small">
+
          Back
        </button>
      </div>
-
    {/if}
-
  </main>
-
{:catch error}
-
  Ethereum wallet not available.
-
{/await}
+
    </div>
+
  {:else}
+
    <div class="input-caption">
+
      Enter your Radicle <strong>vesting contract</strong> address
+
    </div>
+
    <div class="input-main">
+
      <span class="name">
+
        <div>
+
          <input
+
            autofocus
+
            size="40"
+
            placeholder=""
+
            class="subdomain"
+
            disabled={$state === STATE.LOADING}
+
            type="text" bind:value={contractAddress}
+
          />
+
        </div>
+
      </span>
+
      <button
+
        on:click={() => loadContract(config)}
+
        class="primary"
+
        data-waiting={$state === STATE.LOADING || null}
+
        disabled={$state === STATE.LOADING}
+
      >
+
        Load
+
      </button>
+
    </div>
+
  {/if}
+
</main>