Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Button to transfer org ownership
Alexis Sellier committed 4 years ago
commit 19a85b795403d56c14793a24aa05ddd4e3524a1f
parent baee4cea9f4821d6fdbab4d2a597547daf954e10
7 files changed +186 -11
modified src/App.svelte
@@ -14,6 +14,7 @@
  import Modal from '@app/Modal.svelte';

  function handleKeydown(event: KeyboardEvent) {
+
    // TODO: Fix this when there's a modal.
    if (event.key === 'Enter') {
      (document.querySelector('button.primary') as HTMLElement).click();
    }
modified src/Loading.svelte
@@ -10,6 +10,7 @@
    margin: auto 0;
    width: 70px;
    text-align: center;
+
    cursor: wait;
  }
  .spinner.center {
    margin: auto auto;
modified src/Modal.svelte
@@ -2,6 +2,7 @@
  export let floating = false;
  export let error = false;
  export let subtle = false;
+
  export let small = false;
</script>

<style>
@@ -57,6 +58,9 @@
  }
  .modal-subtitle {
    color: var(--color-secondary);
+
    max-width: 90%;
+
    margin: 0 auto;
+
    line-height: 1.5;
  }
  .modal-body {
    overflow-x: hidden;
@@ -68,6 +72,9 @@
    margin-top: 2rem;
    text-align: center;
  }
+
  .modal-small .modal-subtitle {
+
    color: var(--color-foreground);
+
  }
</style>

{#if floating}
@@ -75,14 +82,14 @@
{/if}

<div class:modal-floating={floating}>
-
  <div class="modal" class:error class:modal-subtle={subtle}>
+
  <div class="modal" class:error class:modal-subtle={subtle} class:modal-small={small}>
    <div class="modal-title">
      <slot name="title"></slot>
    </div>
    <div class="modal-subtitle">
      <slot name="subtitle"></slot>
    </div>
-
    {#if $$slots.body}
+
    {#if $$slots.body && !small}
      <div class="modal-body">
        <slot name="body"></slot>
      </div>
modified src/base/orgs/Org.ts
@@ -13,6 +13,7 @@ const orgFactoryAbi = [

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

@@ -43,6 +44,17 @@ export class Org {
      { gasLimit: 200_000 });
  }

+
  async setOwner(address: string, config: Config): Promise<TransactionResponse> {
+
    assert(config.signer);
+

+
    const org = new ethers.Contract(
+
      this.address,
+
      orgAbi,
+
      config.signer
+
    );
+
    return org.setOwner(address);
+
  }
+

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

added src/base/orgs/TransferOwnership.svelte
@@ -0,0 +1,133 @@
+
<script lang="typescript">
+
  import { onMount, createEventDispatcher } from 'svelte';
+
  import Modal from '@app/Modal.svelte';
+
  import type { Config } from '@app/config';
+
  import { formatAddress } from '@app/utils';
+
  import Loading from '@app/Loading.svelte';
+
  import { assert } from '@app/error';
+
  import * as utils from '@app/utils';
+

+
  import type { Org } from './Org';
+

+
  const dispatch = createEventDispatcher();
+

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

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

+
  let newOwner: string | null = null;
+
  let state = State.Idle;
+
  let error: string | null = null;
+
  let input: HTMLInputElement | null = null;
+

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

+
  const resetForm = () => {
+
    state = State.Idle;
+
  };
+

+
  const onSubmit = async () => {
+
    assert(newOwner);
+

+
    if (! utils.isAddress(newOwner)) {
+
      state = State.Failed;
+
      error = `"${newOwner}" is not a valid Ethereum address.`;
+
      return;
+
    }
+

+
    state = State.Signing;
+
    try {
+
      let tx = await org.setOwner(newOwner, config);
+
      state = State.Pending;
+
      await tx.wait();
+
      state = State.Success;
+
    } catch (e) {
+
      console.error(e);
+
      state = State.Failed;
+
      error = e.message;
+
    }
+
  };
+
</script>
+

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

+
    <div slot="subtitle">
+
      The ownership of <strong>{formatAddress(org.address)}</strong> was
+
      successfully transfered to <strong>{newOwner}</strong>.
+
    </div>
+

+
    <div slot="actions">
+
      <button class="small" on:click={() => dispatch('close')}>
+
        Done
+
      </button>
+
    </div>
+
  </Modal>
+
{:else}
+
  <Modal floating error={state == State.Failed} small={state == State.Failed}>
+
    <div slot="title">
+
      🔑
+
      <div>Transfer ownership</div>
+
    </div>
+

+
    <div slot="subtitle">
+
      {#if state == State.Signing}
+
        Please confirm the transaction in your wallet.
+
      {:else if state == State.Pending}
+
        Transaction is being processed by the network...
+
      {:else if state == State.Idle}
+
        Transfer the ownership of Org <strong>{formatAddress(org.address)}</strong> to a new address.
+
      {:else if state == State.Failed}
+
        <div class="error">
+
          {error}
+
        </div>
+
      {/if}
+
    </div>
+

+
    <div slot="body">
+
      {#if state == State.Idle}
+
        <input type="text" size="40" disabled={state !== State.Idle} bind:this={input} bind:value={newOwner} />
+
      {:else if state == State.Pending || state == State.Signing}
+
        <Loading small center />
+
      {:else if state == State.Failed}
+
        <!-- ... -->
+
      {/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 if state == State.Failed}
+
        <button class="small" on:click={resetForm}>
+
          Back
+
        </button>
+
      {:else}
+
        <button class="primary" on:click={onSubmit} disabled={!newOwner || state !== State.Idle}>
+
          Submit
+
        </button>
+

+
        <button class="text" on:click={() => dispatch('close')}>
+
          Cancel
+
        </button>
+
      {/if}
+
    </div>
+
  </Modal>
+
{/if}
modified src/base/orgs/View.svelte
@@ -7,7 +7,6 @@
  import type { Registration } from '@app/base/registrations/registrar';
  import { getRegistration } from '@app/base/registrations/registrar';
  import { parseEnsLabel, explorerLink } from '@app/utils';
-
  import { Org } from './Org';
  import { session } from '@app/session';
  import Loading from '@app/Loading.svelte';
  import Modal from '@app/Modal.svelte';
@@ -16,6 +15,9 @@
  import SetName from '@app/ens/SetName.svelte';
  import * as utils from '@app/utils';

+
  import { Org } from './Org';
+
  import TransferOwnership from './TransferOwnership.svelte';
+

  export let address: string;
  export let config: Config;

@@ -34,6 +36,11 @@
    setNameForm = SetName;
  };

+
  let transferOwnerForm: typeof SvelteComponent | null = null;
+
  const transferOwnership = () => {
+
    transferOwnerForm = TransferOwnership;
+
  };
+

  $: label = name && parseEnsLabel(name, config);
  $: isOwner = (org: Org): boolean => {
    return org.safe === ($session && $session.address);
@@ -61,8 +68,8 @@
  }
  .fields {
    display: grid;
-
    grid-template-columns: 1fr 8fr;
-
    grid-gap: 1rem;
+
    grid-template-columns: 1fr 4fr 2fr;
+
    grid-gap: 1rem 2rem;
  }
  .fields > div {
    justify-self: start;
@@ -119,8 +126,18 @@
      </header>

      <div class="fields">
-
        <div class="label">Address</div><div>{org.address}</div>
+
        <!-- Address -->
+
        <div class="label">Address</div><div>{org.address}</div><div></div>
+
        <!-- Owner -->
        <div class="label">Owner</div><div>{org.safe}</div>
+
        <div>
+
          {#if isOwner(org)}
+
            <button class="tiny secondary" on:click={transferOwnership}>
+
              Transfer
+
            </button>
+
          {/if}
+
        </div>
+
        <!-- Name -->
        <div class="label">Name</div>
        <div>
          {#await org.lookupAddress(config)}
@@ -128,15 +145,18 @@
          {:then name}
            {#if 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 set</span>
            {/if}
          {/await}
        </div>
+
        <div>
+
          {#if isOwner(org)}
+
            <button class="tiny primary" on:click={setName}>
+
              Set
+
            </button>
+
          {/if}
+
        </div>
      </div>
    </main>
  {:else}
@@ -160,6 +180,7 @@
    </Modal>
  {/if}
  <svelte:component this={setNameForm} {org} {config} on:close={() => setNameForm = null} />
+
  <svelte:component this={transferOwnerForm} {org} {config} on:close={() => transferOwnerForm = null} />
{:catch err}
  <Error error={err} />
{/await}
modified src/ens/SetName.svelte
@@ -78,7 +78,7 @@

    <div slot="subtitle">
      {#if mismatchError}
-
        <div class="rror">
+
        <div class="error">
          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