Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Start re-imagining anchors
Alexis Sellier committed 4 years ago
commit b26192f933f99a479f1dbb1611688c7c6f7751cb
parent 3f93c97e54f4e49250412d2c840ef2d8accf52f4
14 files changed +524 -531
modified src/Profile.svelte
@@ -133,6 +133,7 @@
  }
  .members {
    margin-top: 2rem;
+
    padding-bottom: 1rem;
    align-items: center;
    display: flex;
    flex-wrap: wrap;
@@ -141,7 +142,6 @@
    display: flex;
    align-items: center;
    margin-right: 2rem;
-
    margin-bottom: 1rem;
  }
  .members .member:last-child {
    margin-right: 0;
modified src/base/orgs/Org.ts
@@ -7,7 +7,7 @@ import { assert } from '@app/error';
import * as utils from '@app/utils';
import type { Safe } from '@app/utils';
import type { Config } from '@app/config';
-
import type { PendingProject, Project } from '@app/project';
+
import type { PendingAnchor, Anchor } from '@app/project';

const GetProjects = `
  query GetProjects($org: ID!) {
@@ -184,15 +184,16 @@ export class Org {
    return members.includes(address.toLowerCase());
  }

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

    for (const p of result.projects) {
      try {
-
        const proj: Project = {
+
        const proj: Anchor = {
+
          confirmed: true,
          id: utils.formatRadicleId(ethers.utils.arrayify(p.anchor.objectId)),
          anchor: {
            stateHash: utils.formatProjectHash(
@@ -208,39 +209,34 @@ export class Org {
    return projects;
  }

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

    const orgAddr = ethers.utils.getAddress(this.address);
    const response = await config.safe.client.getPendingTransactions(
      ethers.utils.getAddress(this.owner)
    );
-
    const projects: PendingProject[] = [];
+
    const projects: PendingAnchor[] = [];

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

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

-
  async getAllProjects(config: Config): Promise<Array<Project | PendingProject>> {
-
    const result = await Promise.allSettled([
-
      this.getPendingProjects(config),
-
      this.getProjects(config),
-
    ]);
-

-
    return result.flatMap(r => {
-
      return r.status === "fulfilled" ? r.value : [];
-
    });
-
  }
-

  static async getAnchor(orgAddr: string, urn: string, config: Config): Promise<string | null> {
    const org = new ethers.Contract(
      orgAddr,
@@ -377,7 +373,7 @@ export class Org {
  }
}

-
export function parseAnchorTx(data: string, config: Config): Project | null {
+
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 });

@@ -390,7 +386,7 @@ export function parseAnchorTx(data: string, config: Config): Project | null {
    const byteArray = ethers.utils.arrayify(encodedCommitHash);
    const stateHash = utils.formatProjectHash(byteArray);

-
    return { id, anchor: { stateHash } };
+
    return { id, stateHash };
  }
  return null;
}
deleted src/base/orgs/View/Anchor.svelte
@@ -1,233 +0,0 @@
-
<script lang="ts">
-
  import { ethers } from "ethers";
-
  import type { Safe } from "@app/utils";
-
  import type { PendingProject } 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';
-

-
  export let safe: Safe;
-
  export let project: PendingProject;
-
  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 - project.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 = project.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;
-
  }
-
</style>
-

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

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

-
<!-- Check whether the threshold has been matched or passed -->
-
{#if pending <= 0}
-
  <button on:click|stopPropagation={() => {
-
    action = Action.Execute;
-
    state = State.Confirm;
-
  }} class="tiny">
-
    Execute
-
  </button>
-
  <!-- Check whether or not we've signed this proposal -->
-
{:else if isSigned}
-
  <span class="badge safe no-margin">✓ signed</span>
-
{:else}
-
  <button on:click|stopPropagation={() => {
-
    action = Action.Sign;
-
    state = State.Confirm;
-
    }} class="tiny">
-
    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><code>{project.id}</code>
-
          <div>Hash</div><code>{project.anchor.stateHash}</code>
-
        </div>
-
      {:else if state == State.Failed}
-
        <div>{error}</div>
-
      {/if}
-
    </span>
-

-
    <span slot="actions">
-
      {#if state == State.Confirm}
-
        <button class="primary" on:click={() => confirmAnchor(project.safeTxHash)}>
-
          Confirm
-
        </button>
-
        <button class="text" on:click={close}>
-
          Cancel
-
        </button>
-
      {:else if state == State.Success || state == State.Failed}
-
        <button 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><code>{utils.formatHash(project.safeTxHash)}</code>
-
          <div>Quorum</div><code>{project.confirmations.length} of {safe.threshold}</code>
-
        </div>
-
      {:else if state == State.Failed}
-
        <div>{error}</div>
-
      {/if}
-
    </span>
-

-
    <span slot="actions">
-
      {#if state == State.Confirm}
-
        <button class="primary" on:click={() => executeTransaction(project.safeTxHash)}>
-
          Confirm
-
        </button>
-
        <button class="text" on:click={close}>
-
          Cancel
-
        </button>
-
      {:else if state == State.Success || state == State.Failed}
-
        <button on:click={() => {
-
          close();
-
          dispatch("success");
-
        }}>Done</button>
-
      {/if}
-
    </span>
-
  </Modal>
-
{/if}
modified src/base/orgs/View/Projects.svelte
@@ -1,32 +1,44 @@
<script lang="ts">
+
  import { navigate } from "svelte-routing";
+
  import { onMount } from "svelte";
  import type { Config } from "@app/config";
-
  import { Org } from "@app/base/orgs/Org";
-
  import type * as proj from "@app/project";
+
  import * as proj from "@app/project";
  import Loading from "@app/Loading.svelte";
  import Message from "@app/Message.svelte";
  import Widget from '@app/base/projects/Widget.svelte';
-
  import Anchor from './Anchor.svelte';
-
  import { formatCommit } from "@app/utils";
  import type { Profile } from "@app/profile";
+
  import type { ProjectInfo, Anchor, PendingAnchor } from "@app/project";
+
  import { Seed } from "@app/base/seeds/Seed";
+
  import AnchorActions from "@app/base/profiles/AnchorActions.svelte";

  export let profile: Profile;
  export let config: Config;
  export let account: string | null;

-
  const updateRecords = () => {
-
    getProjects = queryProjects;
+
  let anchors: Record<string, Anchor> = {};
+
  let pendingAnchors: Record<string, PendingAnchor> = {};
+

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

+
    anchors = confirmed;
+
    pendingAnchors = pending;
  };

-
  $: queryProjects = async (): Promise<(proj.Project | proj.PendingProject)[]> => {
-
    if (profile.org) {
-
      if (account) {
-
        const result = await profile.org.isMember(account, config);
-
        return result ? profile.org.getAllProjects(config) : profile.org.getProjects(config);
-
      }
-
    }
-
    return [];
+
  const onClick = (project: ProjectInfo) => {
+
    navigate(
+
      proj.path({
+
        urn: project.urn,
+
        addressOrName: profile.name ?? profile.address,
+
        revision: project.head,
+
      })
+
    );
  };
-
  $: getProjects = queryProjects;
+

+
  onMount(loadAnchors);
</script>

<style>
@@ -36,72 +48,36 @@
  .projects .project {
    margin-bottom: 1rem;
  }
-
  .anchor {
+
  .actions {
    display: flex;
    align-items: center;
  }
</style>

<div class="projects">
-
  {#if profile.org}
-
    {#await getProjects()}
-
      <Loading center />
-
    {:then projects}
-
      {#each projects as project}
-
        <div class="project">
-
          {#if "safeTxHash" in project} <!-- Pending project -->
-
            <Widget {project} addressOrName={profile.name ?? profile.address} {config} faded>
-
              <span slot="stateHash">
-
                <span class="mobile">commit {formatCommit(project.anchor.stateHash)}</span>
-
                <span class="desktop">commit {project.anchor.stateHash}</span>
-
              </span>
-
              <span class="anchor" slot="actions">
-
                {#if profile.org.safe && account}
-
                  <Anchor {project} safe={profile.org.safe} on:success={() => updateRecords()} {account} {config} />
-
                {/if}
-
              </span>
-
            </Widget>
-
          {:else} <!-- Anchored project -->
-
            <Widget {project} addressOrName={profile.name ?? profile.address} {config}>
-
              <span slot="stateHash">
-
                <span class="mobile">commit {formatCommit(project.anchor.stateHash)}</span>
-
                <span class="desktop">commit {project.anchor.stateHash}</span>
-
              </span>
-
            </Widget>
-
          {/if}
-
        </div>
-
      {/each}
-
    {:catch err}
-
      <Message error>
-
        <strong>Error: </strong> failed to load anchored projects: {err.message}.
-
      </Message>
-
    {/await}
-
  {:else}
-
    <div class="projects">
-
      {#if profile.anchorsAccount}
-
        {#await Org.get(profile.anchorsAccount, config)}
-
          <Loading center fadeIn />
-
        {:then org}
-
          {#if org}
-
            {#await org.getProjects(config) then projects}
-
              {#each projects as project}
-
                <div class="project">
-
                  <Widget {project} addressOrName={profile.name ?? profile.address} {config}>
-
                    <span slot="stateHash">
-
                      <span class="mobile">commit {formatCommit(project.anchor.stateHash)}</span>
-
                      <span class="desktop">commit {project.anchor.stateHash}</span>
-
                    </span>
-
                  </Widget>
-
                </div>
-
              {/each}
-
            {:catch err}
-
              <Message error>
-
                <strong>Error: </strong> failed to load projects: {err.message}.
-
              </Message>
-
            {/await}
-
          {/if}
-
        {/await}
-
      {/if}
-
    </div>
-
  {/if}
+
  {#await Seed.getProjects(config)}
+
    <Loading center />
+
  {:then projects}
+
    {#each projects as project}
+
      {@const anchor = anchors[project.urn]}
+
      {@const pendingAnchor = pendingAnchors[project.urn]}
+
      <div class="project">
+
        <Widget {project} {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>
+
    {/each}
+
  {:catch err}
+
    <Message error>
+
      <strong>Error: </strong> failed to load projects: {err.message}.
+
    </Message>
+
  {/await}
</div>
added src/base/profiles/AnchorActions.svelte
@@ -0,0 +1,233 @@
+
<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';
+

+
  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;
+
  }
+
</style>
+

+
<span class="confirmations">
+
  {#if pending > 0}
+
    <strong>{pending}</strong> signature(s) pending
+
  {/if}
+
</span>
+

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

+
<!-- Check whether the threshold has been matched or passed -->
+
{#if pending <= 0}
+
  <button on:click|stopPropagation={() => {
+
    action = Action.Execute;
+
    state = State.Confirm;
+
  }} class="tiny">
+
    Execute
+
  </button>
+
  <!-- Check whether or not we've signed this proposal -->
+
{:else if isSigned}
+
  <span class="badge safe no-margin">✓ signed</span>
+
{:else}
+
  <button on:click|stopPropagation={() => {
+
    action = Action.Sign;
+
    state = State.Confirm;
+
    }} class="tiny">
+
    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><code>{anchor.id}</code>
+
          <div>Hash</div><code>{anchor.anchor.stateHash}</code>
+
        </div>
+
      {:else if state == State.Failed}
+
        <div>{error}</div>
+
      {/if}
+
    </span>
+

+
    <span slot="actions">
+
      {#if state == State.Confirm}
+
        <button class="primary" on:click={() => confirmAnchor(anchor.safeTxHash)}>
+
          Confirm
+
        </button>
+
        <button class="text" on:click={close}>
+
          Cancel
+
        </button>
+
      {:else if state == State.Success || state == State.Failed}
+
        <button 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><code>{utils.formatHash(anchor.safeTxHash)}</code>
+
          <div>Quorum</div><code>{anchor.confirmations.length} of {safe.threshold}</code>
+
        </div>
+
      {:else if state == State.Failed}
+
        <div>{error}</div>
+
      {/if}
+
    </span>
+

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

+
  const dispatch = createEventDispatcher();
+

+
  export let anchors: Array<string> = [];
+
  export let head: string;
+
  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: inherit;
+
    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}
+
  <!-- 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/BranchSelector.svelte
@@ -1,11 +1,11 @@
<script lang="ts">
  import { createEventDispatcher } from "svelte";
-
  import { Info, getOid } from "@app/project";
+
  import { ProjectInfo, getOid } from "@app/project";
  import { formatCommit, isOid } from "@app/utils";
  import Dropdown from "@app/Dropdown.svelte";

  export let branches: [string, string][];
-
  export let project: Info;
+
  export let project: ProjectInfo;
  export let revision: string;
  export let toggleDropdown: (input: string) => void;
  export let branchesDropdown = false;
@@ -29,7 +29,7 @@
  $: commit = getOid(project.head, revision, branches);
  $: isLabel = commit == project.head || !isOid(revision);
  $: if (commit == project.head) {
-
    branchLabel = project.meta.defaultBranch;
+
    branchLabel = project.defaultBranch;
  } else if (!isOid(revision)) {
    branchLabel = revision;
  }
@@ -102,7 +102,7 @@
  <!-- If there is no branch listing available, show default branch name if commit is head and else show entire commit -->
  {:else if commit === project.head}
    <div class="stat branch not-allowed">
-
      {project.meta.defaultBranch}
+
      {project.defaultBranch}
    </div>
    <div class="hash">
      {formatCommit(commit)}
modified src/base/projects/Header.svelte
@@ -2,6 +2,7 @@
  import { navigate } from 'svelte-routing';
  import * as utils from '@app/utils';
  import { ProjectContent, getOid, Source } from '@app/project';
+
  import AnchorBadge from '@app/base/profiles/AnchorBadge.svelte';
  import type { Tree } from "@app/project";
  import BranchSelector from './BranchSelector.svelte';
  import PeerSelector from './PeerSelector.svelte';
@@ -63,32 +64,6 @@
  .anchor {
    display: inline-flex;
  }
-
  .anchor-widget {
-
    display: flex;
-
    padding: 0.5rem 0.75rem;
-
    border-radius: inherit;
-
    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-label {
-
    font-family: var(--font-family-monospace);
-
    margin-right: 0.5rem;
-
  }
-
  .anchor-label:last-child {
-
    margin-right: 0;
-
  }
-
  .anchor-latest {
-
    cursor: default;
-
  }
-

  .seed {
    cursor: pointer;
    border-radius: inherit;
@@ -185,34 +160,8 @@
    bind:branchesDropdown={dropdownState.branch}
    on:revisionChanged={(event) => updateRevision(event.detail)} />
  <div class="anchor">
-
    {#if anchors}
-
      <!-- commit is head and latest anchor  -->
-
      {#if commit == anchors[0] && commit === project.head}
-
        <span class="anchor-widget anchor-latest">
-
          <span class="anchor-label" title="{anchors[0]}">latest 🔐</span>
-
        </span>
-
      <!-- commit is not head but latest anchor  -->
-
      {:else if commit == anchors[0] && commit !== project.head}
-
        <span class="anchor-widget" on:click={() => updateRevision(project.head)}>
-
          <span class="anchor-label" title="{anchors[0]}">latest 🔐</span>
-
        </span>
-
      <!-- commit is not head a stale anchor  -->
-
      {:else if anchors.includes(commit)}
-
        <span class="anchor-widget" on:click={() => updateRevision(anchors[0])}>
-
          <span class="anchor-label" title="{commit}">stale 🔒</span>
-
        </span>
-
      <!-- commit is not anchored, could be head or any other commit  -->
-
      {:else}
-
        <span class="anchor-widget not-anchored" on:click={() => updateRevision(anchors[0])}>
-
          <span class="anchor-label">not anchored 🔓</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">
-
        <span class="anchor-label">not anchored 🔓</span>
-
      </span>
-
    {/if}
+
    <AnchorBadge {commit} {anchors}
+
      head={project.head} on:click={(event) => updateRevision(event.detail)} />
  </div>
  {#if config.seed.git.host}
    <span>
modified src/base/projects/View.svelte
@@ -5,7 +5,7 @@
  import Loading from '@app/Loading.svelte';
  import Avatar from '@app/Avatar.svelte';
  import { Profile, ProfileType } from '@app/profile';
-
  import type { Info } from '@app/project';
+
  import type { ProjectInfo } from '@app/project';
  import { formatOrg, formatSeedId, isRadicleId } from '@app/utils';
  import { getOid } from '@app/project';
  import { Seed } from '@app/base/seeds/Seed';
@@ -23,7 +23,7 @@

  let parentName = formatOrg(addressOrName, config);
  let pageTitle = parentName ? `${parentName}/${id}` : id;
-
  let projectInfo: Info | null = null;
+
  let projectInfo: ProjectInfo | null = null;
  let revision: string;
  let content: proj.ProjectContent;
  let path: string;
@@ -40,18 +40,18 @@
    const seedInstance = profile?.seed ?? result?.seed;
    const cfg = seedInstance && seedInstance.valid ? config.withSeed(seedInstance) : config;
    const info = await proj.getInfo(id, cfg);
-
    const urn = isRadicleId(id) ? id : info.meta.urn;
+
    const urn = isRadicleId(id) ? id : info.urn;
    const anchors = await getAllAnchors(config, urn, profile?.anchorsAccount ?? addressOrName);
-
    let branches = Array([info.meta.defaultBranch, info.head]) as [string, string][];
+
    let branches = Array([info.defaultBranch, info.head]) as [string, string][];
    let peers: proj.Peer[] = [];

    projectInfo = info;

    // Checks for delegates returned from seed node, as feature check of the seed node
-
    if (info.meta.delegates) {
+
    if (info.delegates) {
      // Check for selected peer to override available branches.
      if (peer) {
-
        const branchesByPeer = await proj.getBranchesByPeer(urn, peer || info.meta.delegates[0], cfg);
+
        const branchesByPeer = await proj.getBranchesByPeer(urn, peer || info.delegates[0], cfg);
        branches = [...Object.entries(branchesByPeer.heads)];
      }
      peers = await proj.getPeers(urn, cfg);
@@ -61,11 +61,11 @@

  $: if (projectInfo) {
    const baseName = parentName
-
      ? `${parentName}/${projectInfo.meta.name}`
-
      : projectInfo.meta.name;
+
      ? `${parentName}/${projectInfo.name}`
+
      : projectInfo.name;

-
    if (projectInfo.meta.description) {
-
      pageTitle = `${baseName}: ${projectInfo.meta.description}`;
+
    if (projectInfo.description) {
+
      pageTitle = `${baseName}: ${projectInfo.description}`;
    } else {
      pageTitle = baseName;
    }
@@ -162,14 +162,15 @@
          </a>
          <span class="divider">/</span>
        {/if}
-
        <Link to={proj.path({ urn: result.urn, addressOrName, seed })}>{result.project.meta.name}</Link>
+
        <Link to={proj.path({ urn: result.urn, addressOrName, seed })}>{result.project.name}</Link>
        {#if peer}
          <span class="divider" title={peer}>/ {formatSeedId(peer)}</span>
        {/if}
      </div>
      <div class="urn">{result.urn}</div>
-
      <div class="description">{result.project.meta.description}</div>
+
      <div class="description">{result.project.description}</div>
    </header>
+

    {#await proj.getTree(result.urn, getOid(result.project.head, revision, result.branches), "/", config) then tree}
      <Header {tree} {revision} {content} {path}
        source={result}
modified src/base/projects/Widget.svelte
@@ -1,51 +1,10 @@
<script lang="ts">
-
  import { onMount } from 'svelte';
-
  import { navigate } from 'svelte-routing';
-
  import type { Config } from '@app/config';
-
  import * as proj from '@app/project';
-
  import Loading from '@app/Loading.svelte';
-
  import Blockies from '@app/Blockies.svelte';
-
  import { formatRadicleUrn } from '@app/utils';
+
  import type * as proj from '@app/project';
+
  import AnchorBadge from '@app/base/profiles/AnchorBadge.svelte';

-
  enum Status { Loading, Loaded, Error }
-

-
  type State =
-
      { status: Status.Loading }
-
    | { status: Status.Loaded }
-
    | { status: Status.Error; error: string };
-

-
  export let project: proj.Project;
-
  export let config: Config;
-
  export let addressOrName: string | undefined = undefined;
-
  export let seed: string | undefined = undefined;
+
  export let project: proj.ProjectInfo;
  export let faded = false;
-

-
  let state: State = { status: Status.Loading };
-
  let info: proj.Info | null = null;
-

-
  onMount(async () => {
-
    try {
-
      const result = await proj.getInfo(project.id, config);
-
      state = { status: Status.Loaded };
-
      info = result;
-
    } catch (err: any) {
-
      console.debug(err);
-
      state = { status: Status.Error, error: err.message };
-
    }
-
  });
-

-
  const onClick = () => {
-
    if (info) {
-
      navigate(
-
        proj.path({
-
          urn: project.id,
-
          addressOrName,
-
          seed,
-
          revision: project.anchor?.stateHash,
-
        })
-
      );
-
    }
-
  };
+
  export let anchor: proj.Anchor | null = null;
</script>

<style>
@@ -54,18 +13,16 @@
    border: 1px solid var(--color-secondary-faded);
    border-radius: 0.25rem;
    min-width: 36rem;
-
  }
-
  article.has-info {
    cursor: pointer;
  }
  article.project-faded {
    border: 1px dashed var(--color-foreground-subtle);
    cursor: not-allowed;
  }
-
  article.has-info:hover {
+
  article:hover {
    border-color: var(--color-secondary);
  }
-
  article.project-faded.has-info:hover {
+
  article.project-faded:hover {
    border-color: var(--color-foreground-faded);
  }
  article .id {
@@ -101,17 +58,14 @@
    font-family: var(--font-family-monospace);
    font-size: 0.75rem;
  }
+
  article .anchor-badge {
+
    visibility: hidden;
+
  }
  article:hover .id .urn {
    visibility: visible;
  }
-
  article .avatar {
-
    display: flex;
-
    justify-content: center;
-
    align-items: center;
-
    border-radius: 50%;
-
    width: 1.25rem;
-
    height: 1.25rem;
-
    font-size: 0.5rem;
+
  article:hover .anchor-badge {
+
    visibility: visible;
  }
  @media (max-width: 720px) {
    article {
@@ -120,40 +74,29 @@
  }
</style>

-
<article on:click={onClick} class:has-info={info} class:project-faded={faded}>
-
  {#if info}
-
    <div class="id">
-
      <span class="name">{info.meta.name}</span>
-
      <span class="urn desktop">{project.id}</span>
-
    </div>
-
    <div class="description">{info.meta.description}</div>
-
    <div class="anchor">
-
      <span class="commit">
-
        <slot name="stateHash">{info.meta.defaultBranch} {info.head}</slot>
-
      </span>
-
      <span>
-
        {#each info.meta.maintainers as urn}
-
          <span class="avatar">
-
            <Blockies address={urn} />
-
          </span>
-
        {/each}
-
      </span>
-
    </div>
-
  {:else}
-
    <div class="id">
-
      <span class="desktop">{project.id}</span>
-
      <span class="mobile">{formatRadicleUrn(project.id)}</span>
-
      {#if state.status == Status.Loading}
-
        <Loading small />
-
      {/if}
-
    </div>
-
    <div class="anchor">
-
      <span class="commit">
-
        <slot name="stateHash"></slot>
-
      </span>
-
      <span class="actions">
-
        <slot name="actions" />
-
      </span>
-
    </div>
-
  {/if}
+
<article on:click class:project-faded={faded}>
+
  <div class="id">
+
    <span class="name">{project.name}</span>
+
    <span class="urn desktop">{project.urn}</span>
+
  </div>
+
  <div class="description">{project.description}</div>
+
  <div class="anchor">
+
    <span class="commit">
+
      <slot name="stateHash">{project.head}</slot>
+
    </span>
+
    <span class="actions">
+
      <slot name="actions">
+
      </slot>
+
    </span>
+
    <span class="anchor-badge">
+
      <slot name="anchor">
+
        {#if anchor}
+
          <AnchorBadge
+
            commit={project.head}
+
            head={project.head} noText noBg
+
            anchors={[anchor.anchor.stateHash]} />
+
        {/if}
+
      </slot>
+
    </span>
+
  </div>
</article>
modified src/base/seeds/Seed.ts
@@ -1,7 +1,6 @@
import * as api from '@app/api';
import type { Config } from '@app/config';
import * as proj from '@app/project';
-
import type { Project } from "@app/project";
import { isDomain } from '@app/utils';
import { assert } from '@app/error';

@@ -46,11 +45,11 @@ export class Seed {
    return api.get("/peer", {}, config);
  }

-
  static async getProject(urn: string, config: Config): Promise<proj.Info> {
+
  static async getProject(urn: string, config: Config): Promise<proj.ProjectInfo> {
    return proj.getInfo(urn, config);
  }

-
  static async getProjects(config: Config): Promise<Project[]> {
+
  static async getProjects(config: Config): Promise<proj.ProjectInfo[]> {
    const result = await proj.getProjects(config);
    return result.map((project: any) => ({ ...project, id: project.urn }));
  }
modified src/base/seeds/View.svelte
@@ -1,15 +1,24 @@
<script lang="ts">
+
  import { navigate } from "svelte-routing";
  import type { Config } from "@app/config";
  import { Seed } from "@app/base/seeds/Seed";
  import Widget from "@app/base/projects/Widget.svelte";
  import Loading from "@app/Loading.svelte";
  import SeedAddress from "@app/SeedAddress.svelte";
  import NotFound from "@app/NotFound.svelte";
+
  import * as proj from "@app/project";

  export let config: Config;
  export let seedAddress: string;

  config = config.withSeed({ host: seedAddress });
+

+
  const onProjectClick = (project: proj.ProjectInfo) => {
+
    navigate(proj.path({
+
      urn: project.urn,
+
      seed: seedAddress,
+
    }));
+
  };
</script>

<style>
@@ -113,19 +122,15 @@
      <div class="desktop" />
    </div>
    <!-- Seed Projects -->
-
    {#if info.version === "0.2.0"}
-
      {#await Seed.getProjects(config) then projects}
-
        <div class="projects">
-
          {#each projects as project}
-
            <div class="project">
-
              <Widget {project} {config} seed={seedAddress} />
-
            </div>
-
          {/each}
-
        </div>
-
      {/await}
-
    {:else}
-
      <div class="projects subtle">For seed project listing, update http-api to v0.2.0</div>
-
    {/if}
+
    {#await Seed.getProjects(config) then projects}
+
      <div class="projects">
+
        {#each projects as project}
+
          <div class="project">
+
            <Widget {project} on:click={() => onProjectClick(project)} />
+
          </div>
+
        {/each}
+
      </div>
+
    {/await}
  </main>
{:catch}
  <NotFound title={seedAddress} subtitle="Not able to query information from this seed." />
modified src/profile.ts
@@ -1,12 +1,14 @@
import type { EnsProfile } from "@app/base/registrations/registrar";
import type { BasicProfile } from '@datamodels/identity-profile-basic';
import {
-
  isAddress, formatCAIP10Address, formatIpfsFile, resolveEnsProfile, resolveIdxProfile, parseUsername, parseEnsLabel, AddressType, identifyAddress
+
  isAddress, formatCAIP10Address, formatIpfsFile, resolveEnsProfile,
+
  resolveIdxProfile, parseUsername, parseEnsLabel, AddressType, identifyAddress
} from "@app/utils";
import type { Config } from "@app/config";
import type { Seed, InvalidSeed } from "@app/base/seeds/Seed";
import { Org } from "@app/base/orgs/Org";
import { NotFoundError } from "@app/error";
+
import type { Anchor, PendingAnchor } from "@app/project";

export interface IProfile {
  address: string;
@@ -128,6 +130,51 @@ export class Profile {
    else return `${config.ceramic.registry}${formatCAIP10Address(this.profile.address, "eip155", config.network.chainId)}`;
  }

+
  // 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 {};
+
    }
+
  }
+

+
  // 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
@@ -8,12 +8,19 @@ export type Urn = string;
export type Peer = string;
export type Branch = { [key: string]: string };

-
export interface ProjectListing {
-
  name: string;
-
  urn: Urn;
+
export interface Anchor {
+
  confirmed: true;
+
  id: string;
+
  anchor: {
+
    stateHash: string;
+
  };
}
-
export interface Project {
+

+
export interface PendingAnchor {
+
  confirmed: false;
  id: string;
+
  safeTxHash: string; // Safe transaction hash.
+
  confirmations: string[]; // Owner addresses who have confirmed.
  anchor: {
    stateHash: string;
  };
@@ -25,7 +32,7 @@ export interface Source {
  addressOrName: string;
  peer: string;
  config: Config;
-
  project: Info;
+
  project: ProjectInfo;
  peers: Peer[];
  anchors: string[];
  seed: string;
@@ -33,11 +40,6 @@ export interface Source {
  profile?: Profile | null;
}

-
export interface PendingProject extends Project {
-
  safeTxHash: string; // Safe transaction hash.
-
  confirmations: string[]; // Owner addresses who have confirmed.
-
}
-

// Enumerates the space below the Header component in the projects View component
export enum ProjectContent {
  Tree,
@@ -45,12 +47,8 @@ export enum ProjectContent {
  Commit,
}

-
export interface Info {
+
export interface ProjectInfo {
  head: string;
-
  meta: Meta;
-
}
-

-
export interface Meta {
  urn: string;
  name: string;
  description: string;
@@ -99,8 +97,13 @@ export interface Branches {
  heads: Branch;
}

-
export async function getInfo(nameOrUrn: string, config: Config): Promise<Info> {
-
  return api.get(`projects/${nameOrUrn}`, {}, config);
+
export async function getInfo(nameOrUrn: string, config: Config): Promise<ProjectInfo> {
+
  const info = await api.get(`projects/${nameOrUrn}`, {}, config);
+

+
  return {
+
    ...info,
+
    ...info.meta // Nb. This is only needed while we are upgrading to the new http-api.
+
  };
}

export async function getCommits(urn: string, commit: string, config: Config): Promise<CommitsHistory> {
@@ -111,7 +114,7 @@ export async function getCommit(urn: string, commit: string, config: Config): Pr
  return api.get(`projects/${urn}/commits/${commit}`, {}, config);
}

-
export async function getProjects(config: Config): Promise<ProjectListing[]> {
+
export async function getProjects(config: Config): Promise<ProjectInfo[]> {
  return api.get("projects", {}, config);
}