Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add user identities to components
Sebastian Martinez committed 4 years ago
commit 19c4f2625f0195bdb39fb6ea5fc25b780a5f8562
parent 4b9205faa2007c90720ad007cbc4e62e5315d991
18 files changed +287 -198
modified src/Address.svelte
@@ -2,12 +2,11 @@
  import { onMount } from 'svelte';
  import { link } from 'svelte-routing';
  import { ethers } from 'ethers';
-
  import { safeLink, explorerLink, identifyAddress, formatAddress, AddressType } from '@app/utils';
+
  import { safeLink, explorerLink, identifyAddress, formatAddress, AddressType, parseEnsLabel } from '@app/utils';
+
  import { Profile } from '@app/profile';
  import Loading from '@app/Loading.svelte';
  import Avatar from "@app/Avatar.svelte";
  import type { Config } from '@app/config';
-
  import type { Registration } from '@app/base/registrations/registrar';
-
  import { getRegistration } from '@app/base/registrations/registrar';

  export let address: string;
  export let config: Config;
@@ -15,24 +14,21 @@
  export let noBadge = false;
  export let noAvatar = false;
  export let compact = false;
+
  // This property allows components eg. Header.svelte to pass a resolved profile object.
+
  export let profile: Profile | null = null;

  let checksumAddress = compact
    ? formatAddress(address)
    : ethers.utils.getAddress(address);
  let addressType: AddressType | null = null;
-
  let addressName: string | null = null;
-
  let info: Registration | null;

  onMount(async () => {
    identifyAddress(address, config).then((t: AddressType) => addressType = t);
-
    if (resolve) {
-
      addressName = await config.provider.lookupAddress(address);
-
      if (addressName) {
-
        info = await getRegistration(addressName, config);
-
      }
+
    if (resolve && !profile) {
+
      Profile.get(address, config).then(p => profile = p);
    }
  });
-
  $: addressLabel = addressName ?? checksumAddress;
+
  $: addressLabel = profile?.name ? compact ? parseEnsLabel(profile.name, config) : profile.name : checksumAddress;
</script>

<style>
@@ -59,7 +55,7 @@

<div class="address" title={address} class:no-badge={noBadge}>
  {#if !noAvatar}
-
    <Avatar inline source={info?.avatar ?? address} />
+
    <Avatar inline source={profile?.avatar ?? address} />
  {/if}
  {#if addressType === AddressType.Org}
    <a use:link href={`/orgs/${address}`}>{addressLabel}</a>
modified src/Avatar.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
  import { onMount } from 'svelte';
  import { createIcon } from '@app/blockies';
-
  import { isAddress } from '@app/utils';
+
  import { isAddress, isRadicleId } from '@app/utils';
  
  export let source: string;
  export let inline = false;
@@ -10,7 +10,7 @@
  let container: HTMLElement;

  onMount(() => {
-
    if (isAddress(source)) {
+
    if (isAddress(source) || isRadicleId(source)) {
      const seed = source.toLowerCase();
      const avatar = createIcon({
        seed,
@@ -43,7 +43,7 @@
  }
</style>

-
{#if isAddress(source)}
+
{#if isAddress(source) || isRadicleId(source)}
  <div class="avatar" class:inline bind:this={container} class:glowOnHover title={source}/>
{:else}
  <img class="avatar" class:inline src={source} class:glowOnHover alt="avatar"/>
modified src/Form.svelte
@@ -4,6 +4,7 @@
    value: string | null;
    label?: string;
    placeholder?: string;
+
    resolve: boolean;
    editable: boolean;
  }
</script>
@@ -105,7 +106,7 @@
                <a class="link" href="{field.value}" target="_blank">{field.value}</a>
              </span>
            {:else if isAddress(field.value)}
-
              <Address address={field.value} {config} />
+
              <Address resolve={field.resolve} address={field.value} {config} />
            {:else}
              {field.value}
            {/if}
modified src/Header.svelte
@@ -1,17 +1,18 @@
<script lang="ts">
-
  // TODO: Shorten tx hash
  import { link } from "svelte-routing";
-
  import { formatBalance, formatAddress } from "@app/utils";
+
  import { formatAddress, formatBalance } from "@app/utils";
  import { error, Failure } from '@app/error';
  import { disconnectWallet } from "@app/session";
  import type { Session } from '@app/session';
  import Loading from '@app/Loading.svelte';
-
  import Logo from './Logo.svelte';
-
  import Connect from './Connect.svelte';
+
  import Logo from '@app/Logo.svelte';
+
  import Connect from '@app/Connect.svelte';
  import type { Config } from '@app/config';
+
  import { Profile } from "@app/profile";
+
import Avatar from "./Avatar.svelte";

  export let session: Session | null;
-
  export let config: Config | null;
+
  export let config: Config;

  let sessionButton: HTMLElement | null = null;
  let sessionButtonHover = false;
@@ -63,6 +64,11 @@
    text-decoration: none;
  }
  .address {
+
    display: flex;
+
    flex-direction: row;
+
    align-items: center;
+
    justify-content: center;
+
    min-height: 42px;
    margin-left: 2rem;
    width: 9.75rem;
  }
@@ -148,11 +154,15 @@
        on:mouseover={() => sessionButtonHover = true}
        on:mouseout={() => sessionButtonHover = false}
      >
-
        {#if sessionButtonHover}
-
          Disconnect
-
        {:else}
-
          {formatAddress(address)}
-
        {/if}
+
        {#await Profile.get(address, config)}
+
          <Loading small center />
+
        {:then profile}
+
          {#if sessionButtonHover}
+
            Disconnect
+
          {:else}
+
            <Avatar source={profile.avatar ?? address} inline />{formatAddress(address)}
+
          {/if}
+
        {/await}
      </button>
    {:else if config}
      <span class="connect">
modified src/base/orgs/Create.svelte
@@ -130,7 +130,7 @@
        <span><Address address={org.address} {config} /></span>

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

modified src/base/orgs/Index.svelte
@@ -65,7 +65,7 @@
          <Loading center />
        </div>
      {:then orgs}
-
        <List {orgs}>
+
        <List {config} {orgs}>
          <div class="orgs-empty">Orgs you are a member of show up here.</div>
        </List>
      {/await}
@@ -81,7 +81,7 @@
      <Loading center />
    </div>
  {:then orgs}
-
    <List {orgs}>
+
    <List {config} {orgs}>
      <div class="orgs-empty">There are no orgs.</div>
    </List>
  {/await}
modified src/base/orgs/List.svelte
@@ -1,9 +1,15 @@
<script lang="ts">
  import { Link } from 'svelte-routing';
  import type { Org } from '@app/base/orgs/Org';
-
  import Blockies from '@app/Blockies.svelte';
+
  import Avatar from '@app/Avatar.svelte';
+
  import { Profile } from '@app/profile';
+
  import type { Config } from '@app/config';
+
  import Loading from '@app/Loading.svelte';

+
  export let config: Config;
  export let orgs: Org[];
+

+
  const orgsAddresses = orgs.map(org => org.address);
</script>

<style>
@@ -13,14 +19,30 @@
    margin: 3rem;
    display: inline-block;
  }
+
  .list {
+
    display: flex;
+
    flex-direction: row;
+
    flex-wrap: wrap; 
+
  }
+
  .loading {
+
    padding: 3rem 0;
+
  }
</style>

-
{#each orgs as org}
-
  <div class="org">
-
    <Link to={`/orgs/${org.address}`}>
-
      <Blockies glowOnHover address={org.address} />
-
    </Link>
+
{#await Profile.getMulti(orgsAddresses, config)}
+
  <div class="loading">
+
    <Loading center /> 
+
  </div>
+
{:then profiles}
+
  <div class="list">
+
    {#each profiles as profile}
+
      <div class="org">
+
        <Link to={`/orgs/${profile.address}`}>
+
          <Avatar glowOnHover source={profile.avatar ?? profile.address} />
+
        </Link>
+
      </div>
+
    {:else}
+
      <slot />
+
    {/each}
  </div>
-
{:else}
-
  <slot />
-
{/each}
+
{/await}
modified src/base/orgs/Org.ts
@@ -60,10 +60,6 @@ export class Org {
    this.owner = owner;
  }

-
  async lookupAddress(config: Config): Promise<string> {
-
    return await config.provider.lookupAddress(this.address);
-
  }
-

  async setName(name: string, config: Config): Promise<TransactionResponse> {
    assert(config.signer);

modified src/base/orgs/TransferOwnership.svelte
@@ -2,10 +2,9 @@
  import { onMount, createEventDispatcher } from 'svelte';
  import Modal from '@app/Modal.svelte';
  import type { Config } from '@app/config';
-
  import { formatAddress } from '@app/utils';
+
  import { formatAddress, isAddress } from "@app/utils";
  import Loading from '@app/Loading.svelte';
  import { assert } from '@app/error';
-
  import * as utils from '@app/utils';

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

@@ -38,7 +37,7 @@
  const onSubmit = async () => {
    assert(newOwner);

-
    if (! utils.isAddress(newOwner)) {
+
    if (! isAddress(newOwner)) {
      state = State.Failed;
      error = `"${newOwner}" is not a valid Ethereum address.`;
      return;
@@ -58,7 +57,7 @@
  };
</script>

-
{#if state === State.Success}
+
{#if state === State.Success && newOwner}
  <Modal floating small>
    <div slot="title">
modified src/base/orgs/View.svelte
@@ -1,40 +1,29 @@
<script lang="ts">
-
  import * as ethers from 'ethers';
-
  import { onMount } from 'svelte';
  import type { SvelteComponent } from 'svelte';
  import Link from '@app/Link.svelte';
  import type { Config } from '@app/config';
-
  import type { Registration } from '@app/base/registrations/registrar';
-
  import { getRegistration } from '@app/base/registrations/registrar';
  import { parseEnsLabel, explorerLink } from '@app/utils';
  import { session } from '@app/session';
  import Loading from '@app/Loading.svelte';
  import Modal from '@app/Modal.svelte';
  import Error from '@app/Error.svelte';
  import Icon from '@app/Icon.svelte';
-
  import Blockies from '@app/Blockies.svelte';
  import SetName from '@app/ens/SetName.svelte';
  import Project from '@app/base/projects/Widget.svelte';
  import Address from '@app/Address.svelte';
+
  import Avatar from '@app/Avatar.svelte';
  import Message from '@app/Message.svelte';
  import * as utils from '@app/utils';

-
  import { Org } from './Org';
-
  import TransferOwnership from './TransferOwnership.svelte';
+
  import { Org } from '@app/base/orgs/Org';
+
  import TransferOwnership from '@app/base/orgs/TransferOwnership.svelte';
+
  import { Profile } from '@app/profile';

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

-
  let registration: Registration | null = null;
-
  let name: string | null = null;
-

  const back = () => window.history.back();

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

  let setNameForm: typeof SvelteComponent | null = null;
  const setName = () => {
    setNameForm = SetName;
@@ -44,8 +33,6 @@
  const transferOwnership = () => {
    transferOwnerForm = TransferOwnership;
  };
-

-
  $: label = name && parseEnsLabel(name, config);
  $: isOwner = (org: Org): boolean => $session
    ? utils.isAddressEqual(org.owner, $session.address)
    : false;
@@ -97,11 +84,6 @@
    width: 64px;
    height: 64px;
  }
-
  .avatar img {
-
    width: 100%;
-
    height: 100%;
-
    border-radius: 50%;
-
  }
  .links {
    display: flex;
    align-items: center;
@@ -119,6 +101,7 @@
  }
  .members {
    margin-top: 2rem;
+
    align-items: center;
    display: flex;
  }
  .members .member {
@@ -126,9 +109,6 @@
    align-items: center;
    margin-right: 1.5rem;
  }
-
  .members .member a {
-
    border-bottom: none;
-
  }
  .members .member-icon {
    width: 2rem;
    height: 2rem;
@@ -141,33 +121,32 @@
{:then org}
  {#if org}
    <main>
+
      {#await Profile.get(address, config)}
+
        <Loading fadeIn />
+
      {:then profile}
      <header>
        <div class="avatar">
-
          {#if registration && registration.avatar}
-
            <img src={registration.avatar} alt="avatar" />
-
          {:else}
-
            <Blockies address={org.address} />
-
          {/if}
+
          <Avatar source={profile.avatar ?? address} />
        </div>
        <div class="info">
          <span class="title bold">
-
            {registration ? label : ethers.utils.getAddress(address)}
+
            {parseEnsLabel(profile.name, config) ?? address}
          </span>
          <div class="links">
-
            {#if registration}
-
              {#if registration.url}
-
                <a class="url" href={registration.url}>{registration.url}</a>
-
              {/if}
-
              {#if registration.twitter}
-
                <a class="url" href={registration.twitter}>
-
                  <Icon name="twitter" />
-
                </a>
-
              {/if}
-
              {#if registration.github}
-
                <a class="url" href={registration.github}>
-
                  <Icon name="github" />
-
                </a>
-
              {/if}
+
            {#if profile.url}
+
              <a class="url" href={profile.url}>
+
                {profile.url}
+
              </a>
+
            {/if}
+
            {#if profile.twitter}
+
              <a class="url" href={profile.twitter}>
+
                <Icon name="twitter" />
+
              </a>
+
            {/if}
+
            {#if profile.github}
+
              <a class="url" href={profile.github}>
+
                <Icon name="github" />
+
              </a>
            {/if}
          </div>
        </div>
@@ -191,15 +170,11 @@
        <!-- Name -->
        <div class="label">Name</div>
        <div>
-
          {#await org.lookupAddress(config)}
-
            <div class="loading"><Loading small /></div>
-
          {:then name}
-
            {#if name}
-
              <Link to={`/registrations/${label}`}>{name}</Link>
-
            {:else}
-
              <span class="subtle">Not set</span>
-
            {/if}
-
          {/await}
+
          {#if profile.name}
+
            <Link to={`/registrations/${parseEnsLabel(profile.name, config)}`}>{profile.name}</Link>
+
          {:else}
+
            <span class="subtle">Not set</span>
+
          {/if}
        </div>
        <div>
          {#await isAuthorized(org)}
@@ -214,38 +189,43 @@
        </div>
      </div>

-
      <div class="members">
-
        {#await org.getMembers(config)}
-
          <Loading center />
-
        {:then members}
-
          {#each members as address}
-
            <div class="member">
-
              <div class="member-icon">
-
                <Blockies {address} />
-
              </div>
-
              <a href={explorerLink(address, config)} target="_blank" class="member">
-
                {utils.formatAddress(address)}
-
              </a>
+
      {#await org.getMembers(config)}
+
        <Loading center />
+
      {:then members}
+
        {#if members.length > 0}
+
          <div class="members">
+
            {#each members as address}
+
              {#await Profile.get(address, config)}
+
                <Loading small center />
+
              {:then profile}
+
                <div class="member">
+
                  <div class="member-icon">
+
                    <Avatar source={profile.avatar ?? address} />
+
                  </div>
+
                  <Address {address} compact resolve noAvatar {config} />
+
                </div>
+
                {/await}
+
              {/each}
            </div>
-
          {/each}
+
          {/if}
        {/await}
-
      </div>

-
      <div class="projects">
-
        {#await org.getProjects(config)}
-
          <Loading center />
-
        {:then projects}
-
          {#each projects as project}
-
            <div class="project">
-
              <Project {project} org={org.address} {config} />
-
            </div>
-
          {/each}
-
        {:catch err}
-
          <Message error>
-
            <strong>Error: </strong> failed to load projects: {err.message}.
-
          </Message>
-
        {/await}
-
      </div>
+
        <div class="projects">
+
          {#await org.getProjects(config)}
+
            <Loading center />
+
          {:then projects}
+
            {#each projects as project}
+
              <div class="project">
+
                <Project {project} org={org.address} {config} />
+
              </div>
+
            {/each}
+
          {:catch err}
+
            <Message error>
+
              <strong>Error: </strong> failed to load projects: {err.message}.
+
            </Message>
+
          {/await}
+
        </div>
+
      {/await}
    </main>
  {:else}
    <Modal subtle>
modified src/base/projects/View.svelte
@@ -4,7 +4,7 @@
  import * as proj from '@app/project';
  import Loading from '@app/Loading.svelte';
  import Modal from '@app/Modal.svelte';
-
  import Blockies from '@app/Blockies.svelte';
+
  import Avatar from '@app/Avatar.svelte';

  import Browser from './Browser.svelte';

@@ -77,7 +77,7 @@
        <span class="maintainers">
          {#each project.meta.maintainers as user}
            <span class="maintainer">
-
              <Blockies address={user} />
+
              <Avatar source={user} />
            </span>
          {/each}
        </span>
modified src/base/registrations/View.svelte
@@ -45,19 +45,19 @@
        if (r) {
          fields = [
            { name: "owner", placeholder: "",
-
              value: r.owner, editable: false },
+
              value: r.owner, resolve: true, editable: false },
            { name: "address", placeholder: "Not set",
-
              value: r.address, editable: true },
+
              value: r.address, resolve: false, editable: true },
            { name: "seed", placeholder: "Not set",
-
              value: r.seed, editable: true },
+
              value: r.seed, resolve: false, editable: true },
            { name: "url", label: "URL", placeholder: "Not set",
-
              value: r.url, editable: true },
+
              value: r.url, resolve: false, editable: true },
            { name: "avatar", placeholder: "Not set",
-
              value: r.avatar, editable: true },
+
              value: r.avatar, resolve: false, editable: true },
            { name: "twitter", placeholder: "Not set",
-
              value: r.twitter, editable: true },
+
              value: r.twitter, resolve: false, editable: true },
            { name: "github", placeholder: "Not set",
-
              value: r.github, editable: true },
+
              value: r.github, resolve: false, editable: true },
          ];
          state = { status: Status.Found, registration: r };
        } else {
modified src/base/resolver/Query.svelte
@@ -15,21 +15,28 @@
  onMount(async () => {
    if (query) {
      if (ethers.utils.isAddress(query)) {
-
        // Go to org.
-
        navigate(`/orgs/${query}`, { replace: true });
+
        const addressType = query && await utils.identifyAddress(query, config);
+
        if (addressType === utils.AddressType.Org) {
+
          navigate(`/orgs/${query}`, { replace: true });
+
        } else if (addressType === utils.AddressType.EOA) {
+
          navigate(`/users/${query}`, { replace: true });
+
        }
      } else if (utils.isRadicleId(query)) {
        // Go to Radicle entity.
        alert("Radicle IDs are not yet supported");
      } else {
        let label = utils.parseEnsLabel(query, config);
-
        if (label.includes(".")) {
+
        if (label?.includes(".")) {
          error = true;
        } else {
-
          // Jump straight to org, if the ENS entry points to an org. Otherwise just go to the
-
          // registration.
+
          // Jump straight to org, if the ENS entry points to an org. Otherwise it checks if the
+
          // address type is an EOA and jumps to the user page else it just goes to the registration.
          const address = await utils.resolveLabel(label, config);
-
          if (address && await utils.identifyAddress(address, config) === utils.AddressType.Org) {
+
          const addressType = address && await utils.identifyAddress(address, config);
+
          if (addressType === utils.AddressType.Org) {
            navigate(`/orgs/${address}`, { replace: true });
+
          } else if (addressType === utils.AddressType.EOA) {
+
            navigate(`/users/${address}`, { replace: true });
          } else {
            navigate(`/registrations/${label}`, { replace: true });
          }
modified src/base/users/View.svelte
@@ -1,22 +1,13 @@
<script lang="ts">
-
  import { onMount } from 'svelte';
  import type { Config } from '@app/config';
-
  import type { Registration } from '@app/base/registrations/registrar';
-
  import { getRegistration } from '@app/base/registrations/registrar';
  import Icon from '@app/Icon.svelte';
  import Address from '@app/Address.svelte';
  import Avatar from '@app/Avatar.svelte';
+
  import { Profile } from '@app/profile';
+
  import Loading from '@app/Loading.svelte';

  export let address: string;
  export let config: Config;
-
  
-
  let addressName: string | null = null;
-
  let info: Registration | null;
-
  
-
  onMount(async () => {
-
    addressName = await config.provider.lookupAddress(address);
-
    info = await getRegistration(addressName, config);
-
  });
</script>

<style>
@@ -56,24 +47,27 @@
  }
</style>

-
<main>
-
  <header>
-
    <div class="avatar">
-
      <Avatar source={info?.avatar ?? address} />
-
    </div> 
-
    <div class="info">
-
      <span class="title bold"><Address noAvatar {address} {config} resolve/></span>
+
{#await Profile.get(address, config)}
+
  <Loading fadeIn />
+
{:then profile}
+
  <main>
+
    <header>
+
      <div class="avatar">
+
        <Avatar source={profile.avatar ?? address} />
+
      </div> 
+
      <div class="info">
+
        <span class="title bold"><Address compact noAvatar {address} {config} resolve/></span>
        <div class="links">
-
          {#if info?.url}
-
            <a class="url" href={info.url}>{info.url}</a>
+
          {#if profile.url}
+
            <a class="url" href={profile.url}>{profile.url}</a>
          {/if}
-
          {#if info?.twitter}
-
            <a class="url" href={info.twitter}>
+
          {#if profile.twitter}
+
            <a class="url" href={profile.twitter}>
              <Icon name="twitter" />
            </a>
          {/if}
-
          {#if info?.github}
-
            <a class="url" href={info.github}>
+
          {#if profile.github}
+
            <a class="url" href={profile.github}>
              <Icon name="github" />
            </a>
          {/if}
@@ -81,3 +75,4 @@
      </div>
    </header> 
  </main>
+
{/await}
modified src/base/vesting/Index.svelte
@@ -1,12 +1,13 @@
<script lang="ts">
  import { onMount } from 'svelte';
-
  import { formatAddress } from '@app/utils';
  import { State, state } from './state';
  import { getInfo, withdrawVested } from './vesting';
  import type { VestingInfo } from './vesting';
  import type { Session } from '@app/session';
  import type { Config } from '@app/config';
  import Modal from '@app/Modal.svelte';
+
  import Address from '@app/Address.svelte';
+
  import { formatAddress } from '@app/utils';

  let input: HTMLElement;

@@ -64,7 +65,7 @@
            Tokens successfully withdrawn to {formatAddress(info.beneficiary)}.
          {:else}
            <table>
-
              <tr><td class="label">Beneficiary</td><td>{info.beneficiary}</td></tr>
+
              <tr><td class="label">Beneficiary</td><td><Address {config} address={info.beneficiary} compact resolve /></td></tr>
              <tr><td class="label">Allocation</td><td>{info.totalVesting} <strong>{info.symbol}</strong></td></tr>
              <tr><td class="label">Withdrawn</td><td>{info.withdrawn} <strong>{info.symbol}</strong></td></tr>
              <tr><td class="label">Withdrawable</td><td>{info.withdrawableBalance} <strong>{info.symbol}</strong></td></tr>
modified src/config.json
@@ -67,6 +67,7 @@
  },
  "ceramic": { "api": "https://ceramic-clay.3boxlabs.com" },
  "radicle": { "api": "" },
+
  "ipfs": { "gateway": "https://ipfs.io/ipfs/" },
  "abi": {
    "registrar": [
      "function rad() view returns (address)",
added src/profile.ts
@@ -0,0 +1,87 @@
+
import type { Registration } from "@app/base/registrations/registrar";
+
import type { BasicProfile } from "@ceramicstudio/idx-constants";
+
import { formatCAIP10Address, formatIpfsFile, resolveEnsProfile, resolveIdxProfile } from "@app/utils";
+
import type { Config } from "./config";
+

+
export interface IProfile {
+
  ens: Registration | null;
+
  idx: BasicProfile | null;
+
}
+

+
export class Profile {
+
  profile: IProfile;
+
  address: string;
+

+
  constructor(profile: IProfile, address: string) {
+
    this.profile = profile;
+
    this.address = address;
+
  }
+

+
  // Using undefined as return type if nothing to be returned since it works better with <a href> links
+
  get github(): string | undefined {
+
    if (this.profile?.ens?.github) return this.profile.ens.github;
+
    else if (this.profile?.idx?.affiliations) return this.profile.idx?.affiliations.find(item => item === "github");
+
    else return undefined;
+
  }
+

+
  // Using undefined as return type if nothing to be returned since it works better with <a href> links
+
  get twitter(): string | undefined {
+
    if (this.profile?.ens?.twitter) return this.profile.ens.twitter;
+
    else if (this.profile?.idx?.affiliations) return this.profile.idx.affiliations.find(item => item === "twitter");
+
    else return undefined;
+
  }
+

+
  // Using undefined as return type if nothing to be returned since it works better with <a href> links
+
  get url(): string | undefined {
+
    if (this.profile?.ens?.url) return this.profile.ens.url;
+
    else if (this.profile?.idx?.url) return this.profile.idx.url;
+
    else return undefined;
+
  }
+

+
  // Using undefined as return type if nothing to be returned since it works better with <a href> links
+
  get name(): string | undefined {
+
    if (this.profile?.ens?.name) return this.profile.ens.name;
+
    else if (this.profile?.idx?.name) return this.profile.idx.name;
+
    else return undefined;
+
  }
+

+
  // Using undefined as return type if nothing to be returned since it works better with <a href> links
+
  get avatar(): string | undefined {
+
    if (this.profile?.ens?.avatar) return this.profile.ens.avatar;
+
    else if (this.profile?.idx?.image?.original?.src) return formatIpfsFile(this.profile.idx.image.original.src);
+
    else return undefined;
+
  }
+

+
  // Keeping this function private since the desired entrypoint is .get()
+
  private static async lookupAddress(address: string, config: Config): Promise<[IProfile, string]> {
+
    const profile: IProfile = { ens: null, idx: null };
+

+
    try {
+
      const [ens, idx] = await Promise.allSettled([
+
        resolveEnsProfile(address, config),
+
        resolveIdxProfile(formatCAIP10Address(address, "eip155", config.network.chainId), config)
+
      ]);
+

+
      if (ens.status == "fulfilled") profile.ens = ens.value;
+
      if (idx.status == "fulfilled") profile.idx = idx.value;
+
    } catch (error) {
+
      console.error(error);
+
    }
+

+
    return [profile, address];
+
  }
+

+
  static async getMulti(addresses: string[], config: Config): Promise<Profile[]> {
+
    const profilePromises = addresses.map(address => this.lookupAddress(address, config));
+
    const profiles = await Promise.all(profilePromises);
+
    return profiles.map(profile => { return new Profile(...profile); });
+
  }
+

+
  static async get(
+
    address: string,
+
    config: Config,
+
  ): Promise<Profile> {
+
    const profile = await this.lookupAddress(address, config);
+
    return new Profile(...profile);
+
  }
+
}
modified src/utils.ts
@@ -4,15 +4,12 @@ import multibase from 'multibase';
import multihashes from 'multihashes';
import EthersSafe from "@gnosis.pm/safe-core-sdk";
import type { Config } from '@app/config';
+
import config from "@app/config.json";
import { assert } from '@app/error';
import type { Registration } from "@app/base/registrations/registrar";
import { getRegistration } from '@app/base/registrations/registrar';
import type { BasicProfile } from "@ceramicstudio/idx-constants";

-
export interface Profile {
-
  ens: Registration | null;
-
  idx: BasicProfile | null;
-
}

export enum AddressType {
  Contract,
@@ -50,6 +47,11 @@ export function formatAddress(addr: string): string {
  return formatHash(ethers.utils.getAddress(addr));
}

+
export function formatIpfsFile(ipfs: string | undefined): string | undefined {
+
  if (ipfs) return `${config.ipfs.gateway}${ipfs.replace("ipfs://", "")}`;
+
  return undefined;
+
}
+

export function formatHash(hash: string): string {
  return hash.substring(0, 6)
    + '...'
@@ -62,12 +64,14 @@ export function capitalize(s: string): string {
}

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

-
  return label;
+
    return label;
+
  }
}

// Return the current unix time.
@@ -90,6 +94,12 @@ export function isDid(input: string): boolean {
  return /^did:[a-zA-Z0-9]+:[a-zA-Z0-9]+$/.test(input);
}

+
export function isENSName(input: string, config: Config): boolean {
+
  const domain = config.registrar.domain.replace(".", "\\.");
+
  const regEx = new RegExp(`^[a-zA-Z0-9]+.${domain}$`);
+
  return regEx.test(input);
+
}
+

// Check whether the input is an Ethereum address.
export function isAddress(input: string): boolean {
  return ethers.utils.isAddress(input);
@@ -195,27 +205,11 @@ export async function identifyAddress(address: string, config: Config): Promise<
}

// Resolve a label under the radicle domain.
-
export async function resolveLabel(label: string, config: Config): Promise<string | null> {
-
  return config.provider.resolveName(`${label}.${config.registrar.domain}`);
+
export async function resolveLabel(label: string | undefined, config: Config): Promise<string | null> {
+
  if (label) return config.provider.resolveName(`${label}.${config.registrar.domain}`);
+
  return null;
}

-
export async function lookupAddress(address: string, config: Config): Promise<Profile> {
-
  const profile: Profile = { ens: null, idx: null };
-

-
  try {
-
    const [ens, idx] = await Promise.allSettled([
-
      resolveEnsProfile(address, config),
-
      resolveIdxProfile(formatCAIP10Address(address, "eip155", config.network.chainId), config)
-
    ]);
-

-
    if (ens.status == "fulfilled") profile.ens = ens.value;
-
    if (idx.status == "fulfilled") profile.idx = idx.value;
-
  } catch (error) {
-
    console.error(error);
-
  }
-

-
  return profile;
-
}

// Resolves an IDX profile or return null
export async function resolveIdxProfile(caip10: string, config: Config): Promise<BasicProfile | null> {