Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Integrate with Org::setName
Alexis Sellier committed 5 years ago
commit 112df8c12635ee3466048b11d24663aa74f93a49
parent 729bfb4b657e728a0ba820d598c8f96dfe00ce3b
10 files changed +262 -82
modified public/index.css
@@ -149,7 +149,8 @@ button.small {
}
button.tiny {
	min-width: 0;
-
	padding: 0.125rem 0.75rem;
+
	padding: 0 0.75rem;
+
	height: 2rem;
}

a {
modified src/base/orgs/Org.ts
@@ -11,7 +11,10 @@ const orgFactoryAbi = [
  "event OrgCreated(address, address)",
];

-
const orgAbi = ["function owner() view returns (address)"];
+
const orgAbi = [
+
  "function owner() view returns (address)",
+
  "function setName(string, address) returns (bytes32)",
+
];

export class Org {
  address: string
@@ -28,6 +31,16 @@ export class Org {
    return await config.provider.lookupAddress(this.address);
  }

+
  async setName(name: string, config: Config): Promise<TransactionResponse> {
+
    const org = new ethers.Contract(
+
      this.address,
+
      orgAbi,
+
      config.signer
+
    );
+
    return org.setName(name, config.provider.network.ensAddress,
+
      { gasLimit: 200_000 });
+
  }
+

  static fromReceipt(receipt: ContractReceipt): Org | null {
    let event = receipt.events?.find(e => e.event === 'OrgCreated');

modified src/base/orgs/Profile.svelte
@@ -1,28 +1,42 @@
<script lang="typescript">
-
  import { ethers } from 'ethers';
+
  import { onMount } from 'svelte';
+
  import { Link } from 'svelte-routing';
+
  import type { SvelteComponent } from 'svelte';
  import { navigate } from 'svelte-routing';
  import type { Config } from '@app/config';
  import type { Registration } from '@app/base/register/registrar';
  import { getRegistration } from '@app/base/register/registrar';
+
  import { parseEnsLabel } from '@app/utils';
  import { Org } from './Org';
+
  import { session } from '@app/session';
  import Loading from '@app/Loading.svelte';
  import Modal from '@app/Modal.svelte';
  import Error from '@app/Error.svelte';
  import Icon from '@app/Icon.svelte';
+
  import SetName from '@app/ens/SetName.svelte';

-
  export let name: string;
+
  export let address: string;
  export let config: Config;

-
  let address = `${name}.${config.registrar.domain}`;
  let registration: Registration | null = null;
-
  let loading = true;
+
  let name: string | null = null;

-
  getRegistration(name, config).then(r => {
-
    registration = r;
-
    loading = false;
+
  const back = () => navigate("/orgs");
+

+
  onMount(async () => {
+
    name = await config.provider.lookupAddress(address);
+
    registration = await getRegistration(name, config);
  });

-
  const back = () => navigate("/orgs");
+
  let setNameForm: typeof SvelteComponent | null = null;
+
  const setName = () => {
+
    setNameForm = SetName;
+
  };
+

+
  $: label = name && parseEnsLabel(name, config);
+
  $: isOwner = (org: Org): boolean => {
+
    return org.safe === ($session && $session.address);
+
  };
</script>

<style>
@@ -43,15 +57,14 @@
  }
  .fields {
    display: grid;
-
    grid-template-columns: auto auto;
+
    grid-template-columns: 1fr 8fr;
    grid-gap: 1rem;
  }
  .fields > div {
    justify-self: start;
    align-self: center;
-
  }
-
  .actions {
-
    margin-top: 2rem;
+
    height: 2rem;
+
    line-height: 2rem;
  }
  .avatar {
    width: 64px;
@@ -60,7 +73,7 @@
  .links {
    display: flex;
    align-items: center;
-
    justify-content: center;
+
    justify-content: left;
  }
  .url {
    display: flex; /* Ensures correct vertical positioning of icons */
@@ -80,7 +93,7 @@
          </div>
        {/if}
        <div class="info">
-
          <span class="title bold">{address}</span>
+
          <span class="title bold">{label || address}</span>
          <div class="links">
            {#if registration}
              {#if registration.url}
@@ -99,35 +112,28 @@
            {/if}
          </div>
        </div>
-
        <div>
-
          {#if loading}
-
            <Loading small />
-
          {/if}
-
        </div>
      </header>

      <div class="fields">
        <div class="label">Address</div><div>{org.address}</div>
        <div class="label">Owner</div><div>{org.safe}</div>
-
        <div class="label">Reverse Entry</div>
+
        <div class="label">Name</div>
        <div>
          {#await org.lookupAddress(config)}
            <Loading small />
          {:then name}
            {#if name}
-
              {name}
+
              <Link to={`/registrations/${label}`}>{name}</Link>
+
            {:else if isOwner(org)}
+
              <button class="tiny primary" on:click={setName}>
+
                Set
+
              </button>
            {:else}
-
              <span class="subtle">Not registered</span>
+
              <span class="subtle">Not set</span>
            {/if}
          {/await}
        </div>
      </div>
-

-
      <div class="actions">
-
        <button on:click={() => navigate(`/registrations/${name}`)} class="tiny secondary">
-
          View registration &rarr;
-
        </button>
-
      </div>
    </main>
  {:else}
    <Modal subtle>
@@ -140,6 +146,7 @@
      </span>
    </Modal>
  {/if}
+
  <svelte:component this={setNameForm} {org} {config} on:close={() => setNameForm = null} />
{:catch err}
  <Error error={err} />
{/await}
modified src/base/orgs/Routes.svelte
@@ -11,6 +11,6 @@
  <Orgs {config} />
</Route>

-
<Route path="/orgs/:name" let:params>
-
  <Profile {config} name={params.name} />
+
<Route path="/orgs/:address" let:params>
+
  <Profile {config} address={params.address} />
</Route>
modified src/base/register/Register.svelte
@@ -1,10 +1,10 @@
<script lang="typescript">
-
  import { onMount } from 'svelte';
  import { navigate } from "svelte-routing";
  import { registrar } from './registrar';
  import type { Config } from '@app/config';

  import Modal from '@app/Modal.svelte';
+
  import DomainInput from '@app/ens/DomainInput.svelte';

  enum State {
    Idle,
@@ -17,11 +17,6 @@

  let state = State.Idle;
  let inputValue: string;
-
  let inputElement: HTMLElement;
-

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

  function checkAvailability(name: string) {
    state = State.CheckingAvailability;
@@ -42,16 +37,6 @@
    padding-top: 2rem;
    align-self: center;
  }
-
  input.subdomain {
-
    margin-right: 0;
-
    border-top-right-radius: 0;
-
    border-bottom-right-radius: 0;
-
    border-radius: var(--border-radius) 0 0 var(--border-radius);
-
    border-right: none;
-
  }
-
  input.subdomain[disabled] {
-
    color: var(--color-secondary);
-
  }
  div.input-caption {
    font-size: 1.25rem;
    text-align: left;
@@ -69,26 +54,6 @@
  .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;
-
  }
</style>

<main>
@@ -113,17 +78,13 @@
  </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">.{config.registrar.domain}</span>
-
      </div>
+
      <DomainInput
+
        bind:value={inputValue}
+
        autofocus
+
        placeholder=""
+
        disabled={state === State.CheckingAvailability}
+
        root={config.registrar.domain}
+
      />
    </span>
    {#if !inputValue}
      <button disabled class="primary register">
modified src/base/register/Registration.svelte
@@ -27,8 +27,9 @@
  let editable = false;
  let fields: Field[] = [];
  let registration: Registration | null = null;
+
  let name = `${subdomain}.${config.registrar.domain}`;

-
  const loadRegistration = getRegistration(subdomain, config)
+
  const loadRegistration = getRegistration(name, config)
    .then(r => {
      if (r) {
        fields = [
modified src/base/register/registrar.ts
@@ -34,8 +34,7 @@ export interface Registration {
  resolver: EnsResolver
}

-
export async function getRegistration(label: string, config: Config): Promise<Registration | null> {
-
  const name =`${label}.${config.registrar.domain}`;
+
export async function getRegistration(name: string, config: Config): Promise<Registration | null> {
  const resolver = await config.provider.getResolver(name);
  if (! resolver) {
    return null;
added src/ens/DomainInput.svelte
@@ -0,0 +1,61 @@
+
<script lang="typescript">
+
  import { onMount } from 'svelte';
+

+
  export let root: string;
+
  export let placeholder = "";
+
  export let disabled = false;
+
  export let value = "";
+
  export let autofocus = false;
+

+
  let element: HTMLInputElement;
+

+
  onMount(() => {
+
    if (autofocus) {
+
      element.focus();
+
    }
+
  });
+
</script>
+

+
<style>
+
  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;
+
  }
+
  input {
+
    margin: 0;
+
    margin-right: 0;
+
    border-top-right-radius: 0;
+
    border-bottom-right-radius: 0;
+
    border-radius: var(--border-radius) 0 0 var(--border-radius);
+
    border-right: none;
+
  }
+
  input[disabled] {
+
    color: var(--color-secondary);
+
  }
+
  main {
+
    display: flex;
+
    flex-direction: row;
+
    align-items: center;
+
    justify-content: center;
+
  }
+
</style>
+

+
<main>
+
  <input
+
    type="text"
+
    {disabled}
+
    {placeholder}
+
    bind:this={element}
+
    bind:value={value}
+
    on:input
+
    on:click
+
  />
+
  <span class="root">.{root}</span>
+
</main>
added src/ens/SetName.svelte
@@ -0,0 +1,129 @@
+
<script lang="typescript">
+
  import { createEventDispatcher } from 'svelte';
+
  import Modal from '@app/Modal.svelte';
+
  import type { Config } from '@app/config';
+
  import { formatAddress } from '@app/utils';
+
  import DomainInput from '@app/ens/DomainInput.svelte';
+
  import type { Org } from '@app/base/orgs/Org';
+
  import Loading from '@app/Loading.svelte';
+

+
  const dispatch = createEventDispatcher();
+

+
  export let org: Org;
+
  export let config: Config;
+

+
  enum State {
+
    Idle,
+
    Checking,
+
    Signing,
+
    Pending,
+
    Success,
+
    Failed,
+
  }
+

+
  let name: string = "";
+
  let state = State.Idle;
+
  let mismatchError = false; // Set if the name entered does not resolve to the address.
+

+
  const onSubmit = async () => {
+
    state = State.Checking;
+

+
    let domain = `${name}.${config.registrar.domain}`;
+
    let resolved = await config.provider.resolveName(domain);
+

+
    if (resolved === org.address) {
+
      state = State.Signing;
+
      try {
+
        let tx = await org.setName(domain, config);
+
        state = State.Pending;
+
        await tx.wait();
+
        state = State.Success;
+
      } catch (e) {
+
        console.error(e);
+
        state = State.Failed;
+
      }
+
    } else {
+
      state = State.Idle;
+
      mismatchError = true;
+
    }
+
  };
+
</script>
+

+
{#if state === State.Success}
+
  <Modal floating>
+
    <div slot="title">
+
+
    </div>
+

+
    <div slot="subtitle">
+
      The ENS name for {org.address} was set to
+
      <strong>{name}.{config.registrar.domain}</strong>.
+
    </div>
+

+
    <div slot="actions">
+
      <button class="small" on:click={() => dispatch('close')}>
+
        Done
+
      </button>
+
    </div>
+
  </Modal>
+
{:else}
+
  <Modal floating>
+
    <div slot="title">
+
      {#if mismatchError}
+
        🦉
+
      {:else}
+
        🖊️
+
      {/if}
+
    </div>
+

+
    <div slot="subtitle">
+
      {#if mismatchError}
+
        <div class="rror">
+
          The name <strong>{name}.{config.registrar.domain}</strong> does not
+
          resolve to <strong>{formatAddress(org.address)}</strong>. Please update
+
          The ENS record for {name}.{config.registrar.domain} to
+
          point to the correct address.
+
        </div>
+
      {:else if state == State.Signing}
+
        Please confirm the transaction in your wallet.
+
      {:else if state == State.Pending}
+
        Transaction is being processed by the network...
+
      {:else}
+
        Set an ENS name for <strong>{formatAddress(org.address)}</strong>
+
      {/if}
+
    </div>
+

+
    <div slot="body">
+
      {#if state === State.Idle || state === State.Checking}
+
        <DomainInput root={config.registrar.domain} on:input={() => mismatchError = false}
+
          autofocus disabled={state !== State.Idle} bind:value={name} />
+
      {:else}
+
        <Loading small center />
+
      {/if}
+
    </div>
+

+
    <div slot="actions">
+
      {#if state == State.Signing}
+
        <button class="small" on:click={() => dispatch('close')}>
+
          Cancel
+
        </button>
+
      {:else if state == State.Pending}
+
        <button class="small" on:click={() => dispatch('close')}>
+
          Close
+
        </button>
+
      {:else}
+
        <button class="primary" on:click={onSubmit} disabled={!name || state !== State.Idle}>
+
          {#if state === State.Checking}
+
            Checking...
+
          {:else}
+
            Submit
+
          {/if}
+
        </button>
+

+
        <button class="text" on:click={() => dispatch('close')}>
+
          Cancel
+
        </button>
+
      {/if}
+
    </div>
+
  </Modal>
+
{/if}
modified src/utils.ts
@@ -1,5 +1,6 @@
import { ethers } from "ethers";
import type { BigNumber } from "ethers";
+
import type { Config } from '@app/config';

export function formatBalance(n: BigNumber) {
  return ethers.utils.commify(parseFloat(ethers.utils.formatUnits(n)).toFixed(2));
@@ -15,3 +16,10 @@ export function capitalize(s: string) {
  if (s === "") return s;
  return s[0].toUpperCase() + s.substr(1);
}
+

+
// Takes a domain name, eg. 'cloudhead.radicle.eth' and
+
// returns the label, eg. 'cloudhead'.
+
export function parseEnsLabel(name: string, config: Config) {
+
  let domain = config.registrar.domain.replace(".", "\\.");
+
  return name.replace(new RegExp(`\\.${domain}$`), "");
+
}