Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Remove deprecated org parts
Sebastian Martinez committed 3 years ago
commit 271fcde43f4e38e69427647688743e6680e273ca
parent 538c6d90c11223e07972edeb6b1d275add7f097e
24 files changed +258 -1699
modified src/App.svelte
@@ -6,8 +6,6 @@
  import Home from "@app/base/home/Index.svelte";
  import Vesting from "@app/base/vesting/Index.svelte";
  import Registrations from "@app/base/registrations/Routes.svelte";
-
  import Orgs from "@app/base/orgs/Routes.svelte";
-
  import Users from "@app/base/users/Routes.svelte";
  import Seeds from "@app/base/seeds/Routes.svelte";
  import Faucet from "@app/base/faucet/Routes.svelte";
  import Projects from "@app/base/projects/Routes.svelte";
@@ -92,10 +90,8 @@
          <Vesting {config} session={$session} />
        </Route>
        <Registrations {config} session={$session} />
-
        <Orgs />
        <Seeds {config} session={$session} />
        <Faucet {config} />
-
        <Users />
        <Resolver {config} />
        <Route path="/:addressOrName" let:params>
          <Profile addressOrName={params.addressOrName} {config} />
deleted src/Card.svelte
@@ -1,113 +0,0 @@
-
<script lang="ts">
-
  import { Link } from "svelte-routing";
-
  import Avatar from "@app/Avatar.svelte";
-
  import type { Profile } from "@app/profile";
-
  import type { Config } from "@app/config";
-
  import { formatName, formatAddress } from "@app/utils";
-
  import type { Seed } from "@app/base/seeds/Seed";
-

-
  export let profile:
-
    | Profile
-
    | {
-
        address: string;
-
        avatar?: string;
-
        name?: string;
-
      }
-
    | null = null;
-
  export let seed: Seed | null = null;
-
  export let config: Config;
-
  export let path: string;
-
  export let members: string[] = [];
-
</script>
-

-
<style>
-
  .card {
-
    width: 12rem;
-
    height: 10.5rem;
-
    margin-right: 1.5rem;
-
    margin-top: 1.5rem;
-
    margin-bottom: 1.5rem;
-
    padding: 1rem;
-
    display: inline-block;
-
    text-align: center;
-
    border-radius: var(--border-radius);
-
  }
-
  .card::last-child {
-
    margin-right: 0;
-
  }
-
  .card:hover {
-
    background: var(--color-foreground-background-lighter);
-
  }
-
  .card-avatar {
-
    margin: 0 auto;
-
    text-align: center;
-
    width: 4rem;
-
    height: 4rem;
-
  }
-
  .card-label {
-
    color: var(--color-foreground-90);
-
    display: inline-block;
-
    font-weight: var(--font-weight-medium);
-
    margin-top: 0.75rem;
-
    border-radius: var(--border-radius);
-
    padding: 0rem;
-
    text-overflow: ellipsis;
-
    overflow-x: hidden;
-
  }
-
  .card-members {
-
    font-size: var(--font-size-small);
-
    color: var(--color-foreground-faded);
-
  }
-

-
  .card.seed {
-
    width: 14rem;
-
  }
-
  .card.seed .card-label {
-
    max-width: 12rem;
-
  }
-
  .card.seed .seed-emoji {
-
    font-size: 3rem;
-
  }
-

-
  @media (max-width: 720px) {
-
    .card {
-
      margin-right: 0;
-
    }
-
  }
-
</style>
-

-
<Link to={path}>
-
  <div class="card" class:seed={!!seed}>
-
    <div class="card-avatar">
-
      {#if profile}
-
        <Avatar
-
          source={profile.avatar ?? profile.address}
-
          title={profile.address} />
-
      {:else if seed}
-
        <span class="seed-emoji">
-
          {seed.emoji}
-
        </span>
-
      {/if}
-
    </div>
-
    <div class="card-label">
-
      {#if profile}
-
        {#if profile.name}
-
          {formatName(profile.name, config)}
-
        {:else}
-
          {formatAddress(profile.address)}
-
        {/if}
-
      {:else if seed}
-
        {seed.host}
-
      {/if}
-
    </div>
-
    <div class="card-members">
-
      {#if profile}
-
        {#if members.length > 0}
-
          {members.length} member(s)
-
        {:else}
-
          {formatAddress(profile.address)}
-
        {/if}
-
      {/if}
-
    </div>
-
  </div>
-
</Link>
deleted src/Cards.svelte
@@ -1,71 +0,0 @@
-
<script lang="ts">
-
  import { onMount } from "svelte";
-
  import { Profile, ProfileType } from "@app/profile";
-
  import Card from "@app/Card.svelte";
-
  import type { Org } from "@app/base/orgs/Org";
-
  import type { Config } from "@app/config";
-
  import type { Seed } from "@app/base/seeds/Seed";
-

-
  export let config: Config;
-
  export let orgs: Org[] = [];
-
  export let profiles: Profile[] = [];
-
  export let seeds: Seed[] = [];
-

-
  const orgMembers: Record<string, string[]> = {};
-

-
  onMount(async () => {
-
    const promises = orgs.map(org => {
-
      org.getMembers(config).then(members => {
-
        orgMembers[org.address] = members;
-
      });
-
    });
-

-
    Promise.all(promises);
-
  });
-
</script>
-

-
<style>
-
  .list {
-
    display: flex;
-
    flex-direction: row;
-
    flex-wrap: wrap;
-
  }
-
  @media (max-width: 720px) {
-
    .list {
-
      justify-content: center;
-
    }
-
  }
-
</style>
-

-
<div class="list">
-
  {#each orgs as org}
-
    {#await Profile.get(org.name ?? org.address, ProfileType.Minimal, config)}
-
      <Card
-
        profile={{ address: org.address }}
-
        {config}
-
        path={`/${org.address}`} />
-
    {:then profile}
-
      {#if orgMembers[profile.address]?.length}
-
        <Card
-
          {profile}
-
          {config}
-
          path={`/${profile.nameOrAddress}`}
-
          members={orgMembers[profile.address]} />
-
      {:else}
-
        <Card {profile} {config} path={`/${profile.nameOrAddress}`} />
-
      {/if}
-
    {/await}
-
  {/each}
-

-
  {#each profiles as profile}
-
    <Card {profile} {config} path={`/${profile.nameOrAddress}`} />
-
  {/each}
-

-
  {#each seeds as seed}
-
    <Card {seed} {config} path={`/seeds/${seed.host}`} />
-
  {/each}
-

-
  {#if !orgs.length && !profiles.length && !seeds.length}
-
    <slot />
-
  {/if}
-
</div>
deleted src/Options.svelte
@@ -1,92 +0,0 @@
-
<script lang="ts">
-
  import { marked } from "marked";
-
  import { createEventDispatcher } from "svelte";
-
  import { markdownExtensions as extensions } from "@app/utils";
-

-
  export let options: {
-
    label: string;
-
    value: string;
-
    description?: string[];
-
  }[];
-
  export let name: string;
-
  export let selected = "";
-
  export let disabled = false;
-

-
  marked.use({ extensions });
-
  const dispatch = createEventDispatcher();
-
</script>
-

-
<style>
-
  main {
-
    display: flex;
-
    justify-content: center;
-
  }
-
  .options {
-
    text-align: left;
-
  }
-
  .options label {
-
    display: block;
-
    margin-bottom: 1rem;
-
    line-height: 1.5rem;
-
    width: 100%;
-
  }
-
  .options label:last-child {
-
    margin-bottom: 0.5rem;
-
  }
-
  .options label,
-
  .options input {
-
    cursor: pointer;
-
  }
-
  .options input {
-
    margin-right: 0.75rem;
-
    display: inline-block;
-
  }
-
  .options .description {
-
    color: var(--color-secondary);
-
    font-size: var(--font-size-small);
-
    margin-bottom: 2rem;
-
  }
-
  .options .description:last-child {
-
    margin-bottom: 0;
-
  }
-
  .description :global(a) {
-
    text-decoration: underline;
-
  }
-
  .description :global(em) {
-
    font-style: italic;
-
  }
-
  .description :global(strong) {
-
    font-weight: var(--font-weight-medium);
-
  }
-
  .desktop {
-
    display: block !important;
-
  }
-
  @media (max-width: 720px) {
-
    .desktop {
-
      display: none !important;
-
    }
-
  }
-
</style>
-

-
<main>
-
  <div class="options">
-
    {#each options as option}
-
      <label for={option.value}>
-
        <input
-
          type="radio"
-
          {disabled}
-
          checked={selected === option.value}
-
          {name}
-
          id={option.value}
-
          value={option.value}
-
          on:click={() => dispatch("changed", option.value)} />
-
        {option.label}
-
      </label>
-
      {#if option.description}
-
        <div class="description desktop">
-
          {@html marked(option.description.join("\n"))}
-
        </div>
-
      {/if}
-
    {/each}
-
  </div>
-
</main>
modified src/Profile.svelte
@@ -6,7 +6,7 @@
  import Icon from "@app/Icon.svelte";
  import SetName from "@app/ens/SetName.svelte";
  import SeedAddress from "@app/SeedAddress.svelte";
-
  import TransferOwnership from "@app/base/orgs/TransferOwnership.svelte";
+
  import TransferOwnership from "@app/components/TransferOwnership.svelte";
  import Link from "@app/Link.svelte";
  import { getBalance, Profile, ProfileType } from "@app/profile";
  import Loading from "@app/Loading.svelte";
@@ -16,7 +16,7 @@
  import Message from "@app/Message.svelte";
  import Error from "@app/Error.svelte";
  import { User } from "@app/base/users/User";
-
  import Projects from "@app/base/orgs/View/Projects.svelte";
+
  import Projects from "@app/base/seeds/View/Projects.svelte";
  import { MissingReverseRecord, NotFoundError } from "@app/error";
  import NotFound from "@app/NotFound.svelte";
  import RadicleUrn from "@app/RadicleUrn.svelte";
@@ -448,12 +448,7 @@
    {/if}
    {#if profile.seed?.valid}
      <Async fetch={profile.seed.getProjects(10, profile.id)} let:result>
-
        <Projects
-
          {profile}
-
          seed={profile.seed}
-
          {account}
-
          projects={result}
-
          {config} />
+
        <Projects {profile} seed={profile.seed} projects={result} />
      </Async>
    {/if}
  </main>
deleted src/anchors.ts
@@ -1,34 +0,0 @@
-
import { ethers } from "ethers";
-
import type { Config } from "./config";
-
import { decodeRadicleId, formatProjectHash, querySubgraph } from "@app/utils";
-

-
const GetAllAnchors = `
-
  query GetAllAnchors($project: Bytes!, $org: ID!) {
-
    anchors(orderBy: timestamp, orderDirection: desc, where: { objectId: $project, org: $org }) {
-
      multihash
-
      timestamp
-
    }
-
  }
-
`;
-

-
interface AnchorObject {
-
  timestamp: number;
-
  multihash: string;
-
}
-

-
export async function getProjectAnchors(
-
  urn: string,
-
  anchorsStorage: string,
-
  config: Config,
-
): Promise<string[]> {
-
  const unpadded = decodeRadicleId(urn);
-
  const id = ethers.utils.hexZeroPad(unpadded, 32);
-
  const allAnchors = await querySubgraph(config.orgs.subgraph, GetAllAnchors, {
-
    project: id,
-
    org: anchorsStorage,
-
  });
-

-
  return allAnchors.anchors.map((anchor: AnchorObject) =>
-
    formatProjectHash(ethers.utils.arrayify(anchor.multihash)),
-
  );
-
}
deleted src/base/orgs/Create.svelte
@@ -1,225 +0,0 @@
-
<script lang="ts">
-
  import { createEventDispatcher } from "svelte";
-
  import { navigate } from "svelte-routing";
-
  import Modal from "@app/Modal.svelte";
-
  import Error from "@app/Error.svelte";
-
  import type { Err } from "@app/error";
-
  import { Org } from "@app/base/orgs/Org";
-
  import type { Config } from "@app/config";
-
  import Loading from "@app/Loading.svelte";
-
  import Options from "@app/Options.svelte";
-
  import Address from "@app/Address.svelte";
-
  import Button from "@app/Button.svelte";
-

-
  export let config: Config;
-
  export let owner: string;
-

-
  enum State {
-
    Idle,
-
    Signing,
-
    Pending,
-
    Success,
-
  }
-

-
  enum Governance {
-
    Existing = "existing",
-
    Quorum = "quorum",
-
  }
-

-
  const orgTypes = [
-
    {
-
      label: "Multi-signature",
-
      description: [
-
        "Creates an org with a multi-signature contract as its owner, and the specified account as the first member.",
-
        "A [Gnosis Safe](https://gnosis-safe.io) will be deployed for your org.",
-
        "Transactions such as anchoring have to be approved by a quorum of signers.",
-
      ],
-
      value: Governance.Quorum,
-
    },
-
    {
-
      label: "Existing owner",
-
      description: [
-
        `Creates an org with the specified account as the sole owner.`,
-
        `Org transactions such as anchoring are signed and executed from that account.`,
-
        `This option allows for using an existing contract or EOA as the owner of the org.`,
-
      ],
-
      value: Governance.Existing,
-
    },
-
  ];
-

-
  let state = State.Idle;
-
  let error: Err | null = null;
-
  let org: Org | null = null;
-
  let governance = Governance.Quorum;
-

-
  const dispatch = createEventDispatcher();
-
  const createOrg = async () => {
-
    state = State.Signing;
-

-
    try {
-
      const tx =
-
        governance === Governance.Quorum
-
          ? await Org.createMultiSig([owner], 1, config)
-
          : await Org.create(owner, config);
-

-
      state = State.Pending;
-

-
      const receipt = await tx.wait();
-
      org = Org.fromReceipt(receipt);
-
      state = State.Success;
-
    } catch (e: any) {
-
      console.error(e);
-

-
      state = State.Idle;
-
      error = e;
-
    }
-
  };
-

-
  const onGovernanceChanged = (event: { detail: string }) => {
-
    switch (event.detail) {
-
      case "existing":
-
        governance = Governance.Existing;
-
        break;
-
      case "quorum":
-
        governance = Governance.Quorum;
-
        break;
-
    }
-
  };
-
</script>
-

-
<style>
-
  .fields {
-
    display: grid;
-
    grid-template-columns: 1fr 7fr;
-
    grid-gap: 0.5rem 2rem;
-
  }
-
  .fields > span {
-
    justify-self: start;
-
    align-self: center;
-
    height: 2rem;
-
    line-height: 2rem;
-
  }
-
  .configuration {
-
    max-width: 35rem;
-
    text-align: left;
-
  }
-
  .governance {
-
    background-color: var(--color-secondary-background);
-
    border-radius: var(--border-radius);
-
    padding: 1rem 1rem 0rem 1rem;
-
    margin-bottom: 2rem;
-
  }
-
  label[for="address"] {
-
    margin-left: 1.5rem;
-
    font-size: var(--font-size-tiny);
-
    color: var(--color-secondary);
-
  }
-
  input[name="address"] {
-
    margin: 0.5rem 0 0 0;
-
    width: 100%;
-
  }
-
  .notice {
-
    font-size: var(--font-size-small);
-
    border-radius: var(--border-radius);
-
    color: var(--color-yellow);
-
    background-color: var(--color-yellow-background);
-
    padding: 1rem;
-
    margin-bottom: 1rem;
-
  }
-
</style>
-

-
{#if error}
-
  <Error {error} floating on:close />
-
{:else if org}
-
  <!-- Org created -->
-
  <Modal floating on:close>
-
    <span slot="title">🎉</span>
-

-
    <span slot="subtitle">
-
      <span class="txt-bold">Your org was successfully created.</span>
-
    </span>
-

-
    <span slot="body">
-
      <div class="fields">
-
        <span class="label">Address</span>
-
        <span><Address address={org.address} {config} /></span>
-

-
        <span class="label">Owner</span>
-
        <span><Address resolve address={org.owner} {config} /></span>
-
      </div>
-
    </span>
-

-
    <span slot="actions">
-
      <Button
-
        variant="foreground"
-
        on:click={() => navigate(`/${org?.address}`)}>
-
        Done
-
      </Button>
-
    </span>
-
  </Modal>
-
{:else}
-
  <!-- Org creation flow -->
-
  <Modal floating on:close center>
-
    <span slot="title">
-
      <div>🎪</div>
-
      <span>Create an Org</span>
-
    </span>
-

-
    <span slot="subtitle">
-
      {#if state === State.Idle}
-
        <div class="highlight">Select how you'd like to create your org</div>
-
      {:else if state === State.Signing}
-
        <div class="highlight">
-
          Please confirm the transaction in your wallet.
-
        </div>
-
      {:else if state === State.Pending}
-
        <div class="highlight">Waiting for transaction to be processed...</div>
-
      {/if}
-
    </span>
-

-
    <span slot="body">
-
      {#if state === State.Idle}
-
        <div class="configuration">
-
          <div class="notice">
-
            <span class="txt-bold">Notice:</span>
-
            Orgs V1 are being deprecated. It is recommended not to create new orgs
-
            at this point.
-
          </div>
-

-
          <div class="governance">
-
            <Options
-
              name="governance"
-
              disabled={state !== State.Idle}
-
              selected={governance}
-
              options={orgTypes}
-
              on:changed={onGovernanceChanged} />
-
          </div>
-

-
          <label class="input" for="address">Ethereum account address</label>
-
          <input
-
            name="address"
-
            class="small"
-
            type="text"
-
            maxlength="42"
-
            bind:value={owner} />
-
        </div>
-
      {:else}
-
        <Loading center small />
-
      {/if}
-
    </span>
-

-
    <span slot="actions">
-
      {#if state === State.Idle}
-
        <Button
-
          on:click={createOrg}
-
          variant="primary"
-
          waiting={[State.Signing, State.Pending].includes(state)}
-
          disabled={state !== State.Idle}>
-
          Create
-
        </Button>
-

-
        <Button on:click={() => dispatch("close")} variant="text">Close</Button>
-
      {/if}
-
    </span>
-
  </Modal>
-
{/if}
deleted src/base/orgs/Index.svelte
@@ -1,122 +0,0 @@
-
<script lang="ts">
-
  import type { SvelteComponent } from "svelte";
-
  import { session } from "@app/session";
-
  import Create from "@app/base/orgs/Create.svelte";
-
  import { Org } from "@app/base/orgs/Org";
-
  import type { Config } from "@app/config";
-
  import Loading from "@app/Loading.svelte";
-
  import Message from "@app/Message.svelte";
-
  import Cards from "@app/Cards.svelte";
-
  import { setOpenGraphMetaTag } from "@app/utils";
-
  import Button from "@app/Button.svelte";
-

-
  export let config: Config;
-

-
  setOpenGraphMetaTag([
-
    { prop: "og:title", content: "Radicle Orgs" },
-
    { prop: "og:description", content: "Orgs of the Radicle Network" },
-
    { prop: "og:url", content: window.location.href },
-
  ]);
-

-
  const onCreate = () => (modal = Create);
-
  let modal: typeof SvelteComponent | null = null;
-

-
  $: account = $session && $session.address;
-
</script>
-

-
<style>
-
  main {
-
    width: 100%;
-
    padding: 3rem;
-
    display: block;
-
    align-items: space-between;
-
    justify-content: space-between;
-
  }
-
  main header {
-
    color: var(--color-secondary);
-
    font-size: var(--font-size-medium);
-
    display: flex;
-
    align-items: center;
-
    padding: 1rem 0;
-
    gap: 1.5rem;
-
  }
-

-
  .my-orgs {
-
    margin-bottom: 2rem;
-
  }
-
  .orgs-empty {
-
    padding: 2rem 0 1rem 0;
-
  }
-

-
  .loading {
-
    padding: 2rem 0;
-
  }
-
</style>
-

-
<svelte:head>
-
  <title>Radicle: Orgs</title>
-
</svelte:head>
-

-
<main>
-
  {#if account}
-
    <div class="my-orgs">
-
      <header>
-
        <span>
-
          My <span class="txt-bold">Orgs</span>
-
        </span>
-
        <Button variant="secondary" on:click={onCreate} disabled={!account}>
-
          Create
-
        </Button>
-
      </header>
-

-
      {#await Org.getOrgsByMember(account, config)}
-
        <div class="loading">
-
          <Loading center />
-
        </div>
-
      {:then orgs}
-
        <Cards {config} {orgs}>
-
          <div class="orgs-empty txt-missing">
-
            Orgs you are a member of show up here.
-
          </div>
-
        </Cards>
-
      {:catch}
-
        <div>
-
          <Message error>
-
            <span class="txt-bold">Error:</span>
-
            failed to load orgs.
-
          </Message>
-
        </div>
-
      {/await}
-
    </div>
-
  {/if}
-

-
  <header>
-
    <span>
-
      <span class="txt-bold">Orgs</span>
-
      of the Radicle network
-
    </span>
-
  </header>
-

-
  {#await Org.getAll(config)}
-
    <div class="loading">
-
      <Loading center />
-
    </div>
-
  {:then orgs}
-
    <Cards {config} {orgs}>
-
      <div class="orgs-empty">There are no orgs.</div>
-
    </Cards>
-
  {:catch}
-
    <div>
-
      <Message error>
-
        <span class="txt-bold">Error:</span>
-
        failed to load orgs.
-
      </Message>
-
    </div>
-
  {/await}
-
</main>
-

-
<svelte:component
-
  this={modal}
-
  owner={account}
-
  {config}
-
  on:close={() => (modal = null)} />
modified src/base/orgs/Org.ts
@@ -1,37 +1,12 @@
import * as ethers from "ethers";
import type { TransactionResponse } from "@ethersproject/providers";
-
import type { ContractReceipt } from "@ethersproject/contracts";
import { OperationType } from "@gnosis.pm/safe-core-sdk-types";

import { assert } from "@app/error";
import * as utils from "@app/utils";
import * as cache from "@app/cache";
-
import type { SafeMultisigTransactionListResponse } from "@gnosis.pm/safe-service-client";
-
import type SafeServiceClient from "@gnosis.pm/safe-service-client";
import type { Safe } from "@app/utils";
import type { Config } from "@app/config";
-
import type { PendingAnchor, Anchor } from "@app/project";
-

-
const GetProjects = `
-
  query GetProjects($org: ID!) {
-
    projects(where: { org: $org }) {
-
      anchor {
-
        objectId
-
        multihash
-
        timestamp
-
      }
-
    }
-
  }
-
`;
-

-
const GetOrgs = `
-
  query GetOrgs {
-
    orgs {
-
      id
-
      owner
-
    }
-
  }
-
`;

const GetSafesByOwners = `
  query GetSafesByOwners($owners: [String!]!) {
@@ -198,119 +173,6 @@ export class Org {
    return members.includes(address.toLowerCase());
  }

-
  async getProjects(config: Config): Promise<Anchor[]> {
-
    const result = await utils.querySubgraph(
-
      config.orgs.subgraph,
-
      GetProjects,
-
      { org: this.address },
-
    );
-
    const projects: Anchor[] = [];
-

-
    for (const p of result.projects) {
-
      try {
-
        const proj: Anchor = {
-
          confirmed: true,
-
          id: utils.formatRadicleId(ethers.utils.arrayify(p.anchor.objectId)),
-
          anchor: {
-
            stateHash: utils.formatProjectHash(
-
              ethers.utils.arrayify(p.anchor.multihash),
-
            ),
-
          },
-
        };
-
        projects.push(proj);
-
      } catch (e) {
-
        console.error(e);
-
      }
-
    }
-
    return projects;
-
  }
-

-
  async getPendingProjects(config: Config): Promise<PendingAnchor[]> {
-
    if (!config.safe.client) return [];
-

-
    try {
-
      const orgAddr = ethers.utils.getAddress(this.address);
-
      const response = await getPendingProjects(
-
        ethers.utils.getAddress(this.owner),
-
        config.safe.client,
-
      );
-
      const projects: PendingAnchor[] = [];
-

-
      for (const tx of response.results || []) {
-
        if (tx.data && tx.to === orgAddr) {
-
          const anchor = parseAnchorTx(tx.data, config);
-
          const confirmations = tx.confirmations?.map(t => t.owner) || [];
-

-
          if (anchor) {
-
            projects.push({
-
              id: anchor.id,
-
              anchor: { stateHash: anchor.stateHash },
-
              confirmations,
-
              safeTxHash: tx.safeTxHash,
-
              confirmed: false,
-
            });
-
          }
-
        }
-
      }
-
      return projects;
-
    } catch {
-
      return [];
-
    }
-
  }
-

-
  static async getAnchor(
-
    orgAddr: string,
-
    urn: string,
-
    config: Config,
-
  ): Promise<string | null> {
-
    const org = new ethers.Contract(orgAddr, config.abi.org, config.provider);
-
    const unpadded = utils.decodeRadicleId(urn);
-
    const id = ethers.utils.zeroPad(unpadded, 32);
-

-
    try {
-
      const [, hash] = await org.anchors(id);
-
      const anchor = utils.formatProjectHash(ethers.utils.arrayify(hash));
-

-
      return anchor;
-
    } catch (e) {
-
      console.error(e);
-
      return null;
-
    }
-
  }
-

-
  static async getMulti(ids: string[], config: Config): Promise<Array<Org>> {
-
    const results = await Promise.all(ids.map(addr => Org.get(addr, config)));
-
    const orgs = results.filter((org): org is Org => org !== null);
-

-
    return orgs;
-
  }
-

-
  static async getAll(config: Config): Promise<Array<Org>> {
-
    const result = await utils.querySubgraph(config.orgs.subgraph, GetOrgs, {});
-
    const orgs: Org[] = [];
-

-
    for (const o of result.orgs) {
-
      try {
-
        orgs.push(new Org(o.id, o.owner));
-
      } catch (e) {
-
        console.error(e);
-
      }
-
    }
-
    return orgs;
-
  }
-

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

-
    if (event && event.args) {
-
      const address = event.args[0];
-
      const owner = event.args[1];
-

-
      return new Org(address, owner);
-
    }
-
    return null;
-
  }
-

  static async get(addressOrName: string, config: Config): Promise<Org | null> {
    const org = await getOrgContract(addressOrName, config);

@@ -354,62 +216,6 @@ export class Org {

    return orgs.map(o => new Org(o.id, o.owner));
  }
-

-
  static async createMultiSig(
-
    owners: [string],
-
    threshold: number,
-
    config: Config,
-
  ): Promise<TransactionResponse> {
-
    assert(config.signer);
-

-
    const orgFactory = new ethers.Contract(
-
      config.orgFactory.address,
-
      config.abi.orgFactory,
-
      config.signer,
-
    );
-

-
    return orgFactory["createOrg(address[],uint256)"](owners, threshold, {
-
      gasLimit: config.gasLimits.createOrg,
-
    });
-
  }
-

-
  static async create(
-
    owner: string,
-
    config: Config,
-
  ): Promise<TransactionResponse> {
-
    assert(config.signer);
-

-
    const orgFactory = new ethers.Contract(
-
      config.orgFactory.address,
-
      config.abi.orgFactory,
-
      config.signer,
-
    );
-

-
    return orgFactory["createOrg(address)"](owner, {
-
      gasLimit: config.gasLimits.createOrg,
-
    });
-
  }
-
}
-

-
export function parseAnchorTx(
-
  data: string,
-
  config: Config,
-
): { id: string; stateHash: string } | null {
-
  const iface = new ethers.utils.Interface(config.abi.org);
-
  const parsedTx = iface.parseTransaction({ data });
-

-
  if (parsedTx.name === "anchor") {
-
    const encodedProjectUrn = parsedTx.args[0];
-
    const encodedCommitHash = parsedTx.args[2];
-
    const id = utils.formatRadicleId(
-
      ethers.utils.arrayify(`${encodedProjectUrn}`),
-
    );
-
    const byteArray = ethers.utils.arrayify(encodedCommitHash);
-
    const stateHash = utils.formatProjectHash(byteArray);
-

-
    return { id, stateHash };
-
  }
-
  return null;
}

export const getOrgContract = cache.cached(
@@ -425,20 +231,3 @@ export const resolveOrgOwner = cache.cached(
  },
  org => org.address,
);
-

-
export const getPendingProjects = cache.cached(
-
  async (
-
    owner: string,
-
    client: SafeServiceClient,
-
  ): Promise<SafeMultisigTransactionListResponse> => {
-
    try {
-
      return await client.getPendingTransactions(
-
        ethers.utils.getAddress(owner),
-
      );
-
    } catch (e) {
-
      return { count: 0, results: [] };
-
    }
-
  },
-
  owner => owner,
-
  { max: 1000, ttl: 5 * 60 * 1000 }, // Cache results for 5 minutes.
-
);
deleted src/base/orgs/Routes.svelte
@@ -1,15 +0,0 @@
-
<script lang="ts">
-
  import { Route } from "svelte-routing";
-
  import Redirect from "@app/Redirect.svelte";
-
  import NotFound from "@app/NotFound.svelte";
-
</script>
-

-
<Route path="/orgs">
-
  <NotFound
-
    title="404"
-
    subtitle="Radicle Orgs are in the process of being re-designed." />
-
</Route>
-

-
<Route path="/orgs/:addressOrName" let:params>
-
  <Redirect to="/{params.addressOrName}" />
-
</Route>
deleted src/base/orgs/TransferOwnership.svelte
@@ -1,183 +0,0 @@
-
<script lang="ts">
-
  import { onMount, createEventDispatcher } from "svelte";
-
  import Modal from "@app/Modal.svelte";
-
  import type { Config } from "@app/config";
-
  import { formatAddress, isAddress } from "@app/utils";
-
  import Loading from "@app/Loading.svelte";
-
  import { assert } from "@app/error";
-
  import * as utils from "@app/utils";
-
  import Address from "@app/Address.svelte";
-
  import Button from "@app/Button.svelte";
-

-
  import type { Org } from "./Org";
-

-
  const dispatch = createEventDispatcher();
-

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

-
  enum State {
-
    Idle,
-

-
    // Single sig states.
-
    Signing,
-
    Pending,
-
    Success,
-

-
    // Multi sig states.
-
    Proposing,
-
    Proposed,
-

-
    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 (!isAddress(newOwner)) {
-
      state = State.Failed;
-
      error = `"${newOwner}" is not a valid Ethereum address.`;
-
      return;
-
    }
-

-
    try {
-
      if (org && (await utils.isSafe(org.owner, config))) {
-
        state = State.Proposing;
-
        await org.setOwnerMultisig(newOwner, config);
-
        state = State.Proposed;
-
      } else {
-
        state = State.Signing;
-
        const tx = await org.setOwner(newOwner, config);
-
        state = State.Pending;
-
        await tx.wait();
-
        state = State.Success;
-
      }
-
    } catch (e: any) {
-
      console.error(e);
-
      state = State.Failed;
-
      error = e.message;
-
    }
-
  };
-
</script>
-

-
{#if state === State.Success && newOwner}
-
  <Modal floating small>
-
    <div slot="title">✅</div>
-

-
    <div slot="subtitle">
-
      The ownership of <span class="txt-bold">
-
        {formatAddress(org.address)}
-
      </span>
-
      was successfully transfered to
-
      <span class="txt-bold">{newOwner}</span>
-
      .
-
    </div>
-

-
    <div slot="actions">
-
      <Button variant="secondary" on:click={() => dispatch("close")}>
-
        Done
-
      </Button>
-
    </div>
-
  </Modal>
-
{:else if state === State.Proposed && org}
-
  <Modal floating>
-
    <div slot="title">🪴</div>
-

-
    <div slot="subtitle">
-
      <p>
-
        The transaction to set the owner of <span class="txt-bold">
-
          {formatAddress(org.address)}
-
        </span>
-
        to
-
        <span class="txt-bold">{newOwner}</span>
-
        was proposed to:
-
      </p>
-
      <p><Address address={org.owner} {config} compact /></p>
-
    </div>
-

-
    <div slot="actions">
-
      <Button variant="secondary" 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}
-
        Waiting for transaction to be processed...
-
      {:else if state === State.Proposing && org}
-
        Proposal is being submitted to the safe
-
        <span class="txt-bold">{formatAddress(org.owner)}</span>
-
        , please sign the transaction in your wallet.
-
      {:else if state === State.Idle}
-
        Transfer the ownership of Org <span class="txt-bold">
-
          {formatAddress(org.address)}
-
        </span>
-
        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.Proposing || state === State.Signing}
-
        <Loading small center />
-
      {:else if state === State.Failed}
-
        <!-- ... -->
-
      {/if}
-
    </div>
-

-
    <div slot="actions">
-
      {#if state === State.Signing}
-
        <Button variant="text" on:click={() => dispatch("close")}>
-
          Cancel
-
        </Button>
-
      {:else if state === State.Pending}
-
        <Button variant="text" on:click={() => dispatch("close")}>Close</Button>
-
      {:else if state === State.Failed}
-
        <Button variant="negative" on:click={resetForm}>Back</Button>
-
      {:else}
-
        <Button
-
          variant="primary"
-
          on:click={onSubmit}
-
          disabled={!newOwner || state !== State.Idle}>
-
          Submit
-
        </Button>
-

-
        <Button variant="text" on:click={() => dispatch("close")}>
-
          Cancel
-
        </Button>
-
      {/if}
-
    </div>
-
  </Modal>
-
{/if}
deleted src/base/orgs/View/Projects.svelte
@@ -1,104 +0,0 @@
-
<script lang="ts">
-
  import { navigate } from "svelte-routing";
-
  import { onMount } from "svelte";
-
  import type { Config } from "@app/config";
-
  import * as proj from "@app/project";
-
  import Widget from "@app/base/projects/Widget.svelte";
-
  import type { Profile } from "@app/profile";
-
  import type { ProjectInfo, Anchor, PendingAnchor } from "@app/project";
-
  import type { Seed } from "@app/base/seeds/Seed";
-
  import AnchorActions from "@app/base/profiles/AnchorActions.svelte";
-
  import List from "@app/List.svelte";
-

-
  export let seed: Seed;
-
  export let profile: Profile | null = null;
-
  export let account: string | null = null;
-
  export let projects: proj.ProjectInfo[];
-
  export let config: Config;
-

-
  let anchors: Record<string, Anchor> = {};
-
  let pendingAnchors: Record<string, PendingAnchor> = {};
-
  // A pointer to the current page of projects added to the listing
-
  let page = 0;
-

-
  const loadAnchors = async () => {
-
    if (profile) {
-
      const [pending, confirmed] = await Promise.all([
-
        profile.pendingAnchors(config),
-
        profile.confirmedAnchors(config),
-
      ]);
-

-
      anchors = confirmed;
-
      pendingAnchors = pending;
-
    }
-
  };
-

-
  const fetchMoreProjects = async (): Promise<proj.ProjectInfo[]> => {
-
    const projects = await proj.Project.getProjects(seed.api, {
-
      perPage: 10,
-
      page: (page += 1),
-
    });
-
    if (projects.length > 0) {
-
      return projects;
-
    }
-

-
    // We return an empty array, for when no more projects are found, since List is looking for an iterable.
-
    return [];
-
  };
-

-
  const onClick = (project: ProjectInfo) => {
-
    navigate(
-
      proj.path({
-
        urn: project.urn,
-
        seed: seed?.host,
-
        profile: profile?.name ?? profile?.address,
-
        revision: project.head,
-
      }),
-
    );
-
  };
-

-
  onMount(loadAnchors);
-
</script>
-

-
<style>
-
  .projects {
-
    margin-top: 1rem;
-
  }
-
  .projects .project {
-
    margin-bottom: 0.5rem;
-
  }
-
  .actions {
-
    display: flex;
-
    align-items: center;
-
  }
-
</style>
-

-
<div class="projects">
-
  <List items={projects} query={fetchMoreProjects}>
-
    <svelte:fragment slot="list" let:items>
-
      {#each items as project}
-
        {@const anchor = anchors[project.urn]}
-
        {@const pendingAnchor = pendingAnchors[project.urn]}
-
        {#if project.head}
-
          <div class="project">
-
            <Widget {project} {seed} {anchor} on:click={() => onClick(project)}>
-
              <span class="actions" slot="actions">
-
                {#if profile?.org?.safe && account && anchor}
-
                  {#if pendingAnchor}
-
                    <!-- Pending anchor -->
-
                    <AnchorActions
-
                      {account}
-
                      {config}
-
                      anchor={pendingAnchor}
-
                      safe={profile.org.safe}
-
                      on:success={() => loadAnchors()} />
-
                  {/if}
-
                {/if}
-
              </span>
-
            </Widget>
-
          </div>
-
        {/if}
-
      {/each}
-
    </svelte:fragment>
-
  </List>
-
</div>
deleted src/base/profiles/AnchorActions.svelte
@@ -1,267 +0,0 @@
-
<script lang="ts">
-
  import { ethers } from "ethers";
-
  import type { Safe } from "@app/utils";
-
  import type { PendingAnchor } from "@app/project";
-
  import type { Config } from "@app/config";
-
  import * as utils from "@app/utils";
-
  import Modal from "@app/Modal.svelte";
-
  import Avatar from "@app/Avatar.svelte";
-
  import { createEventDispatcher } from "svelte";
-
  import Badge from "@app/Badge.svelte";
-
  import Button from "@app/Button.svelte";
-

-
  export let safe: Safe;
-
  export let anchor: PendingAnchor;
-
  export let account: string;
-
  export let config: Config;
-

-
  enum State {
-
    Idle,
-
    Confirm,
-
    Signing,
-
    Submitting,
-
    Success,
-
    Execute,
-
    Failed,
-
  }
-

-
  enum Action {
-
    Sign,
-
    Execute,
-
  }
-

-
  const dispatch = createEventDispatcher();
-
  let state = State.Idle;
-
  let error: string | null = null;
-
  let action: null | Action = null;
-

-
  const close = () => {
-
    action = null;
-
    state = State.Idle;
-
  };
-

-
  const pending = safe.threshold - anchor.confirmations.length;
-
  const executeTransaction = async (safeTxHash: string) => {
-
    try {
-
      action = Action.Execute;
-
      state = State.Signing;
-
      const txResult = await utils.executeSignedSafeTransaction(
-
        safe.address,
-
        safeTxHash,
-
        config,
-
      );
-

-
      state = State.Submitting;
-
      await txResult.transactionResponse?.wait();
-

-
      state = State.Success;
-
    } catch (err: any) {
-
      console.error(err);
-
      error = err.message;
-
      state = State.Failed;
-
    }
-
  };
-

-
  const confirmAnchor = async (safeTxHash: string) => {
-
    try {
-
      action = Action.Sign;
-
      state = State.Signing;
-
      const signature = await utils.signSafeTransaction(
-
        safe.address,
-
        safeTxHash,
-
        config,
-
      );
-

-
      state = State.Submitting;
-
      await config.safe.client?.confirmTransaction(safeTxHash, signature.data);
-

-
      state = State.Success;
-
    } catch (err: any) {
-
      console.error(err);
-
      error = err.message;
-
      state = State.Failed;
-
    }
-
  };
-

-
  $: isSigned = anchor.confirmations.includes(ethers.utils.getAddress(account));
-
</script>
-

-
<style>
-
  .confirmations {
-
    margin-right: 0.5rem;
-
  }
-
  .table {
-
    display: grid;
-
    grid-template-columns: 5rem 4fr;
-
    grid-gap: 1rem;
-
    text-align: left;
-
  }
-
  .table > *:nth-child(odd) {
-
    /* Labels */
-
    color: var(--color-secondary);
-
  }
-
  .table > *:nth-child(even) {
-
    /* Values */
-
    display: flex;
-
    align-items: center;
-
    justify-content: left;
-
  }
-
  .avatars {
-
    display: flex;
-
    margin-right: 0.75rem;
-
  }
-
</style>
-

-
<span class="confirmations">
-
  {#if pending > 0}
-
    <span class="txt-bold">{pending}</span>
-
    signature(s) pending
-
  {/if}
-
</span>
-

-
<div class="avatars">
-
  {#each anchor.confirmations as signee}
-
    <Avatar inline source={signee} title={signee} />
-
  {/each}
-
</div>
-

-
<!-- Check whether the threshold has been matched or passed -->
-
{#if pending <= 0}
-
  <Button
-
    on:click={() => {
-
      action = Action.Execute;
-
      state = State.Confirm;
-
    }}
-
    variant="foreground"
-
    size="small">
-
    <Avatar inline source={account} title={account} /> Execute
-
  </Button>
-
  <!-- Check whether or not we've signed this proposal -->
-
{:else if isSigned}
-
  <Badge variant="caution">✓ signed</Badge>
-
{:else}
-
  <Button
-
    on:click={() => {
-
      action = Action.Sign;
-
      state = State.Confirm;
-
    }}
-
    variant="foreground"
-
    size="small">
-
    Confirm
-
  </Button>
-
{/if}
-

-
<!-- We've initiated an action -->
-
{#if state !== State.Idle && action === Action.Sign}
-
  <Modal floating>
-
    <span slot="title">
-
      <div>⚓</div>
-
      <div>Anchor project</div>
-
    </span>
-

-
    <span slot="subtitle">
-
      {#if state === State.Confirm}
-
        <span>Initiate the transaction...</span>
-
      {:else if state === State.Signing}
-
        <span>Sign the transaction in your wallet...</span>
-
      {:else if state === State.Submitting}
-
        <span>Transaction is being confirmed...</span>
-
      {:else if state === State.Success}
-
        <span>Transaction confirmed.</span>
-
      {:else if state === State.Failed}
-
        <span>Transaction failed</span>
-
      {/if}
-
    </span>
-

-
    <span slot="body">
-
      {#if state === State.Confirm}
-
        <div class="table">
-
          <div>Project</div>
-
          <span class="txt-monospace">{anchor.id}</span>
-
          <div>Hash</div>
-
          <span class="txt-monospace">{anchor.anchor.stateHash}</span>
-
        </div>
-
      {:else if state === State.Failed}
-
        <div>{error}</div>
-
      {/if}
-
    </span>
-

-
    <span slot="actions">
-
      {#if state === State.Confirm}
-
        <Button
-
          variant="primary"
-
          on:click={() => confirmAnchor(anchor.safeTxHash)}>
-
          Confirm
-
        </Button>
-
        <Button variant="text" on:click={close}>Cancel</Button>
-
      {:else if state === State.Success || state === State.Failed}
-
        <Button
-
          variant="foreground"
-
          on:click={() => {
-
            close();
-
            dispatch("success");
-
          }}>
-
          Done
-
        </Button>
-
      {/if}
-
    </span>
-
  </Modal>
-
{:else if state !== State.Idle && action === Action.Execute}
-
  <Modal floating>
-
    <span slot="title">
-
      <div>⚡</div>
-
      <div>Execute Safe Transaction</div>
-
    </span>
-

-
    <span slot="subtitle">
-
      {#if state === State.Confirm}
-
        <span>Initiate the transaction...</span>
-
      {:else if state === State.Signing}
-
        <span>Sign the transaction in your wallet...</span>
-
      {:else if state === State.Submitting}
-
        <span>Transaction is being confirmed...</span>
-
      {:else if state === State.Success}
-
        <span>Transaction confirmed.</span>
-
      {:else if state === State.Failed}
-
        <span>Transaction failed</span>
-
      {/if}
-
    </span>
-

-
    <span slot="body">
-
      {#if state === State.Confirm}
-
        <div class="table">
-
          <div>TxHash</div>
-
          <span class="txt-monospace">
-
            {utils.formatHash(anchor.safeTxHash)}
-
          </span>
-
          <div>Quorum</div>
-
          <span class="txt-monospace">
-
            {anchor.confirmations.length} of {safe.threshold}
-
          </span>
-
        </div>
-
      {:else if state === State.Failed}
-
        <div>{error}</div>
-
      {/if}
-
    </span>
-

-
    <span slot="actions">
-
      {#if state === State.Confirm}
-
        <Button
-
          variant="primary"
-
          on:click={() => executeTransaction(anchor.safeTxHash)}>
-
          Confirm
-
        </Button>
-
        <Button variant="text" on:click={close}>Cancel</Button>
-
      {:else if state === State.Success || state === State.Failed}
-
        <Button
-
          variant="foreground"
-
          on:click={() => {
-
            close();
-
            dispatch("success");
-
          }}>
-
          Done
-
        </Button>
-
      {/if}
-
    </span>
-
  </Modal>
-
{/if}
deleted src/base/profiles/AnchorBadge.svelte
@@ -1,93 +0,0 @@
-
<script lang="ts">
-
  import { createEventDispatcher } from "svelte";
-

-
  const dispatch = createEventDispatcher();
-

-
  export let anchors: Array<string> = [];
-
  export let head: string | null;
-
  export let commit: string;
-
  export let noText = false;
-
  export let noBg = false;
-

-
  const text = !noText;
-
</script>
-

-
<style>
-
  .anchor-widget {
-
    display: flex;
-
    padding: 0.5rem 0.75rem;
-
    border-radius: var(--border-radius-small);
-
    color: var(--color-tertiary);
-
    background-color: var(--color-tertiary-background);
-
    cursor: pointer;
-
  }
-
  .anchor-widget.not-allowed {
-
    cursor: not-allowed;
-
  }
-
  .anchor-widget.not-anchored {
-
    color: var(--color-foreground-faded);
-
    background-color: var(--color-foreground-background);
-
  }
-
  .anchor-widget.no-bg {
-
    background: none !important;
-
    padding: 0 !important;
-
  }
-
  .anchor-label {
-
    font-family: var(--font-family-monospace);
-
    margin-right: 0.5rem;
-
  }
-
  .anchor-label:last-child {
-
    margin-right: 0;
-
  }
-
  .anchor-latest {
-
    cursor: default;
-
  }
-
</style>
-

-
{#if anchors && head}
-
  <!-- commit is head and latest anchor  -->
-
  {#if commit === anchors[0] && commit === head}
-
    <span class="anchor-widget anchor-latest" class:no-bg={noBg}>
-
      <span class="anchor-label" title={anchors[0]}>
-
        {#if text}latest&nbsp;{/if}🔐
-
      </span>
-
    </span>
-
    <!-- commit is not head but latest anchor  -->
-
  {:else if commit === anchors[0] && commit !== head}
-
    <span
-
      class="anchor-widget"
-
      class:no-bg={noBg}
-
      on:click={() => dispatch("click", head)}>
-
      <span class="anchor-label" title={anchors[0]}>
-
        {#if text}latest&nbsp;{/if}🔐
-
      </span>
-
    </span>
-
    <!-- commit is not head a stale anchor  -->
-
  {:else if anchors.includes(commit)}
-
    <span
-
      class="anchor-widget"
-
      class:no-bg={noBg}
-
      on:click={() => dispatch("click", anchors[0])}>
-
      <span class="anchor-label" title={commit}>
-
        {#if text}stale&nbsp;{/if}🔒
-
      </span>
-
    </span>
-
    <!-- commit is not anchored, could be head or any other commit  -->
-
  {:else}
-
    <span
-
      class="anchor-widget not-anchored"
-
      class:no-bg={noBg}
-
      on:click={() => dispatch("click", anchors[0])}>
-
      <span class="anchor-label">
-
        {#if text}not anchored&nbsp;{/if}🔓
-
      </span>
-
    </span>
-
  {/if}
-
{:else}
-
  <!-- commit is not head and neither an anchor, and there are no anchors available  -->
-
  <span class="anchor-widget not-anchored not-allowed" class:no-bg={noBg}>
-
    <span class="anchor-label">
-
      {#if text}not anchored&nbsp;{/if}🔓
-
    </span>
-
  </span>
-
{/if}
modified src/base/projects/Header.svelte
@@ -2,7 +2,6 @@
  import type { Writable } from "svelte/store";
  import { navigate } from "svelte-routing";
  import { Browser, ProjectContent, Project } from "@app/project";
-
  import AnchorBadge from "@app/base/profiles/AnchorBadge.svelte";
  import BranchSelector from "@app/base/projects/BranchSelector.svelte";
  import CloneButton from "@app/base/projects/CloneButton.svelte";
  import PeerSelector from "@app/base/projects/PeerSelector.svelte";
@@ -12,9 +11,8 @@
  export let tree: Tree;
  export let commit: string;
  export let browserStore: Writable<Browser>;
-
  export let noAnchor = false;

-
  const { urn, peers, branches, seed, anchors } = project;
+
  const { urn, peers, branches, seed } = project;

  $: browser = $browserStore;
  $: revision = browser.revision || commit;
@@ -54,9 +52,6 @@
    border-radius: var(--border-radius-small);
    min-width: max-content;
  }
-
  .anchor {
-
    display: inline-flex;
-
  }
  .clickable {
    cursor: pointer;
  }
@@ -104,16 +99,6 @@
    {revision}
    on:branchChanged={event => updateRevision(event.detail)} />

-
  {#if !noAnchor}
-
    <div class="anchor widget">
-
      <AnchorBadge
-
        {commit}
-
        {anchors}
-
        head={project.head}
-
        on:click={event => updateRevision(event.detail)} />
-
    </div>
-
  {/if}
-

  {#if seed.git.host}
    <CloneButton seedHost={seed.git.host} {urn} />
  {/if}
modified src/base/projects/Project.svelte
@@ -74,7 +74,7 @@
  {#await project.getRoot(revision)}
    <Loading center />
  {:then { tree, commit }}
-
    <Header {tree} {commit} {browserStore} {project} noAnchor />
+
    <Header {tree} {commit} {browserStore} {project} />

    {#if content === proj.ProjectContent.Tree}
      <Browser {project} {commit} {tree} {browserStore} />
modified src/base/projects/Widget.svelte
@@ -1,6 +1,5 @@
<script lang="ts">
  import type * as proj from "@app/project";
-
  import AnchorBadge from "@app/base/profiles/AnchorBadge.svelte";
  import Diagram from "@app/Diagram.svelte";
  import { groupCommitsByWeek } from "@app/commit";
  import type { Host } from "@app/api";
@@ -10,7 +9,6 @@
  export let project: proj.ProjectInfo;
  export let seed: { api: Host };
  export let faded = false;
-
  export let anchor: proj.Anchor | null = null;
  export let compact = false;

  const loadCommits = async () => {
@@ -69,9 +67,6 @@
  article:hover {
    border-color: var(--color-secondary);
  }
-
  article:hover .anchor {
-
    display: block;
-
  }
  article:hover .activity {
    display: none !important;
  }
@@ -94,22 +89,10 @@
    display: flex;
    align-items: center;
  }
-
  article .anchor-info {
-
    display: flex;
-
    align-items: center;
-
  }
-
  article .actions {
-
    margin-right: 1rem;
-
  }
-
  article .commit,
-
  article .actions {
+
  article .commit {
    font-family: var(--font-family-monospace);
  }
-
  article.project-faded .anchor {
-
    color: var(--color-foreground-faded);
-
  }
-
  article .id,
-
  article .anchor {
+
  article .id {
    display: flex;
    justify-content: space-between;
  }
@@ -120,15 +103,9 @@
    font-family: var(--font-family-monospace);
    font-size: var(--font-size-tiny);
  }
-
  article .anchor-badge {
-
    display: none;
-
  }
  article:hover .id .urn {
    visibility: visible;
  }
-
  article:hover .anchor-badge {
-
    display: block;
-
  }
  @media (max-width: 720px) {
    article {
      min-width: 0;
@@ -164,25 +141,6 @@
      <div class="id">
        <span class="urn desktop">{project.urn}</span>
      </div>
-
      <div class="anchor">
-
        <span class="anchor-info">
-
          <span class="actions">
-
            <slot name="actions" />
-
          </span>
-
          <span class="anchor-badge">
-
            <slot name="anchor">
-
              {#if anchor && project.head}
-
                <AnchorBadge
-
                  commit={project.head}
-
                  head={project.head}
-
                  noText
-
                  noBg
-
                  anchors={[anchor.anchor.stateHash]} />
-
              {/if}
-
            </slot>
-
          </span>
-
        </span>
-
      </div>
      {#await loadCommits() then points}
        <div class="desktop activity">
          <Diagram
modified src/base/seeds/View.svelte
@@ -6,7 +6,7 @@
  import SeedAddress from "@app/SeedAddress.svelte";
  import NotFound from "@app/NotFound.svelte";
  import Clipboard from "@app/Clipboard.svelte";
-
  import Projects from "@app/base/orgs/View/Projects.svelte";
+
  import Projects from "@app/base/seeds/View/Projects.svelte";
  import type { Session } from "@app/session";
  import Address from "@app/Address.svelte";
  import SiweConnect from "@app/SiweConnect.svelte";
@@ -158,7 +158,7 @@
    </div>
    <!-- Seed Projects -->
    <Async fetch={Project.getProjects(seedHost, { perPage: 10 })} let:result>
-
      <Projects {seed} {config} projects={result} />
+
      <Projects {seed} projects={result} />
    </Async>
  </main>
{:catch}
added src/base/seeds/View/Projects.svelte
@@ -0,0 +1,63 @@
+
<script lang="ts">
+
  import { navigate } from "svelte-routing";
+
  import * as proj from "@app/project";
+
  import Widget from "@app/base/projects/Widget.svelte";
+
  import type { Profile } from "@app/profile";
+
  import type { ProjectInfo } from "@app/project";
+
  import type { Seed } from "@app/base/seeds/Seed";
+
  import List from "@app/List.svelte";
+

+
  export let seed: Seed;
+
  export let profile: Profile | null = null;
+
  export let projects: proj.ProjectInfo[];
+

+
  // A pointer to the current page of projects added to the listing
+
  let page = 0;
+

+
  const fetchMoreProjects = async (): Promise<proj.ProjectInfo[]> => {
+
    const projects = await proj.Project.getProjects(seed.api, {
+
      perPage: 10,
+
      page: (page += 1),
+
    });
+
    if (projects.length > 0) {
+
      return projects;
+
    }
+

+
    // We return an empty array, for when no more projects are found, since List is looking for an iterable.
+
    return [];
+
  };
+

+
  const onClick = (project: ProjectInfo) => {
+
    navigate(
+
      proj.path({
+
        urn: project.urn,
+
        seed: seed?.host,
+
        profile: profile?.name ?? profile?.address,
+
        revision: project.head,
+
      }),
+
    );
+
  };
+
</script>
+

+
<style>
+
  .projects {
+
    margin-top: 1rem;
+
  }
+
  .projects .project {
+
    margin-bottom: 0.5rem;
+
  }
+
</style>
+

+
<div class="projects">
+
  <List items={projects} query={fetchMoreProjects}>
+
    <svelte:fragment slot="list" let:items>
+
      {#each items as project}
+
        {#if project.head}
+
          <div class="project">
+
            <Widget {project} {seed} on:click={() => onClick(project)} />
+
          </div>
+
        {/if}
+
      {/each}
+
    </svelte:fragment>
+
  </List>
+
</div>
deleted src/base/users/Routes.svelte
@@ -1,8 +0,0 @@
-
<script lang="ts">
-
  import { Route } from "svelte-routing";
-
  import Redirect from "@app/Redirect.svelte";
-
</script>
-

-
<Route path="/users/:addressOrName" let:params>
-
  <Redirect to="/{params.addressOrName}" />
-
</Route>
added src/components/TransferOwnership.svelte
@@ -0,0 +1,183 @@
+
<script lang="ts">
+
  import { onMount, createEventDispatcher } from "svelte";
+
  import Modal from "@app/Modal.svelte";
+
  import type { Config } from "@app/config";
+
  import { formatAddress, isAddress } from "@app/utils";
+
  import Loading from "@app/Loading.svelte";
+
  import { assert } from "@app/error";
+
  import * as utils from "@app/utils";
+
  import Address from "@app/Address.svelte";
+
  import Button from "@app/Button.svelte";
+

+
  import type { Org } from "@app/base/orgs/Org";
+

+
  const dispatch = createEventDispatcher();
+

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

+
  enum State {
+
    Idle,
+

+
    // Single sig states.
+
    Signing,
+
    Pending,
+
    Success,
+

+
    // Multi sig states.
+
    Proposing,
+
    Proposed,
+

+
    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 (!isAddress(newOwner)) {
+
      state = State.Failed;
+
      error = `"${newOwner}" is not a valid Ethereum address.`;
+
      return;
+
    }
+

+
    try {
+
      if (org && (await utils.isSafe(org.owner, config))) {
+
        state = State.Proposing;
+
        await org.setOwnerMultisig(newOwner, config);
+
        state = State.Proposed;
+
      } else {
+
        state = State.Signing;
+
        const tx = await org.setOwner(newOwner, config);
+
        state = State.Pending;
+
        await tx.wait();
+
        state = State.Success;
+
      }
+
    } catch (e: any) {
+
      console.error(e);
+
      state = State.Failed;
+
      error = e.message;
+
    }
+
  };
+
</script>
+

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

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

+
    <div slot="actions">
+
      <Button variant="secondary" on:click={() => dispatch("close")}>
+
        Done
+
      </Button>
+
    </div>
+
  </Modal>
+
{:else if state === State.Proposed && org}
+
  <Modal floating>
+
    <div slot="title">🪴</div>
+

+
    <div slot="subtitle">
+
      <p>
+
        The transaction to set the owner of <span class="txt-bold">
+
          {formatAddress(org.address)}
+
        </span>
+
        to
+
        <span class="txt-bold">{newOwner}</span>
+
        was proposed to:
+
      </p>
+
      <p><Address address={org.owner} {config} compact /></p>
+
    </div>
+

+
    <div slot="actions">
+
      <Button variant="secondary" 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}
+
        Waiting for transaction to be processed...
+
      {:else if state === State.Proposing && org}
+
        Proposal is being submitted to the safe
+
        <span class="txt-bold">{formatAddress(org.owner)}</span>
+
        , please sign the transaction in your wallet.
+
      {:else if state === State.Idle}
+
        Transfer the ownership of Org <span class="txt-bold">
+
          {formatAddress(org.address)}
+
        </span>
+
        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.Proposing || state === State.Signing}
+
        <Loading small center />
+
      {:else if state === State.Failed}
+
        <!-- ... -->
+
      {/if}
+
    </div>
+

+
    <div slot="actions">
+
      {#if state === State.Signing}
+
        <Button variant="text" on:click={() => dispatch("close")}>
+
          Cancel
+
        </Button>
+
      {:else if state === State.Pending}
+
        <Button variant="text" on:click={() => dispatch("close")}>Close</Button>
+
      {:else if state === State.Failed}
+
        <Button variant="negative" on:click={resetForm}>Back</Button>
+
      {:else}
+
        <Button
+
          variant="primary"
+
          on:click={onSubmit}
+
          disabled={!newOwner || state !== State.Idle}>
+
          Submit
+
        </Button>
+

+
        <Button variant="text" on:click={() => dispatch("close")}>
+
          Cancel
+
        </Button>
+
      {/if}
+
    </div>
+
  </Modal>
+
{/if}
modified src/profile.ts
@@ -15,8 +15,6 @@ import { cached } from "@app/cache";
import type { Seed, InvalidSeed } from "@app/base/seeds/Seed";
import { Org } from "@app/base/orgs/Org";
import { NotFoundError, MissingReverseRecord } from "@app/error";
-
import { getProjectAnchors } from "@app/anchors";
-
import type { Anchor, PendingAnchor } from "@app/project";

export interface IProfile {
  address: string;
@@ -153,63 +151,6 @@ export class Profile {
    }
  }

-
  // Get confirmed anchors.
-
  async confirmedAnchors(config: Config): Promise<Record<string, Anchor>> {
-
    const org = await this.getAnchorsOrg(config);
-

-
    if (org) {
-
      const result = await org.getProjects(config);
-
      const anchors: Record<string, Anchor> = {};
-

-
      for (const anchor of result) {
-
        anchors[anchor.id] = anchor;
-
      }
-
      return anchors;
-
    } else {
-
      return {};
-
    }
-
  }
-

-
  // Get pending anchors.
-
  async pendingAnchors(config: Config): Promise<Record<string, PendingAnchor>> {
-
    const org = await this.getAnchorsOrg(config);
-

-
    if (org) {
-
      const result = await org.getPendingProjects(config);
-
      const anchors: Record<string, PendingAnchor> = {};
-

-
      for (const anchor of result) {
-
        anchors[anchor.id] = anchor;
-
      }
-
      return anchors;
-
    } else {
-
      return {};
-
    }
-
  }
-

-
  async confirmedProjectAnchors(
-
    urn: string,
-
    config: Config,
-
  ): Promise<string[]> {
-
    const storage = this.anchorsAccount || this.org?.address;
-

-
    if (storage) {
-
      return await getProjectAnchors(urn, storage, config);
-
    }
-
    return [];
-
  }
-

-
  // Get the anchors account as an org, or the org, if available.
-
  private async getAnchorsOrg(config: Config): Promise<Org | null> {
-
    if (this.anchorsAccount) {
-
      return await Org.get(this.anchorsAccount, config);
-
    } else if (this.org) {
-
      return this.org;
-
    } else {
-
      return null;
-
    }
-
  }
-

  // Keeping this function private since the desired entrypoint is .get()
  // All addresses returned from this function should be lowercase.
  private static async lookupProfile(
modified src/project.ts
@@ -22,24 +22,6 @@ export type Delegate =
      id: PeerId;
    };

-
export interface Anchor {
-
  confirmed: true;
-
  id: string;
-
  anchor: {
-
    stateHash: string;
-
  };
-
}
-

-
export interface PendingAnchor {
-
  confirmed: false;
-
  id: string;
-
  safeTxHash: string; // Safe transaction hash.
-
  confirmations: string[]; // Owner addresses who have confirmed.
-
  anchor: {
-
    stateHash: string;
-
  };
-
}
-

// Enumerates the space below the Header component in the projects View component
export enum ProjectContent {
  Tree,
@@ -277,7 +259,6 @@ export class Project implements ProjectInfo {
  peers: Peer[];
  branches: Branches;
  profile: Profile | null;
-
  anchors: string[];
  // At the moment we still have seed nodes which won't return neither patches or issues
  patches?: number;
  issues?: number;
@@ -289,7 +270,6 @@ export class Project implements ProjectInfo {
    peers: Peer[],
    branches: Branches,
    profile: Profile | null,
-
    anchors: string[],
  ) {
    this.urn = urn;
    this.head = info.head;
@@ -304,7 +284,6 @@ export class Project implements ProjectInfo {
    this.patches = info.patches;
    this.issues = info.issues;
    this.profile = profile;
-
    this.anchors = anchors;
  }

  async getRoot(
@@ -480,9 +459,6 @@ export class Project implements ProjectInfo {

    const info = await Project.getInfo(id, seed.api);
    const urn = isRadicleId(id) ? id : info.urn;
-
    const anchors = profile
-
      ? await profile.confirmedProjectAnchors(urn, config)
-
      : [];

    // Older versions of http-api don't include the URN.
    if (!info.urn) info.urn = urn;
@@ -503,7 +479,7 @@ export class Project implements ProjectInfo {
      }
    }

-
    return new Project(urn, info, seed, peers, remote.heads, profile, anchors);
+
    return new Project(urn, info, seed, peers, remote.heads, profile);
  }

  static async getMulti(
modified vite.config.ts
@@ -1,8 +1,8 @@
///<reference types="vitest" />
import path from "path";
-
import { UserConfig } from "vite";
+
import type { UserConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
-
import { ViteDevServer } from "vite";
+
import type { ViteDevServer } from "vite";
import IstanbulPlugin from "vite-plugin-istanbul";
import history from "connect-history-api-fallback";
import type { Request, Response } from "express-serve-static-core";