Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Reduce number of requests for profiles
Alexis Sellier committed 4 years ago
commit 5226591a63533c90b2f6f396381288ce2083b7a5
parent b165ee95875d8336a5ead0512ce058a5437b4518
11 files changed +135 -60
modified src/Address.svelte
@@ -3,7 +3,7 @@
  import { link } from 'svelte-routing';
  import { ethers } from 'ethers';
  import { safeLink, explorerLink, identifyAddress, formatAddress, AddressType, parseEnsLabel } from '@app/utils';
-
  import { Profile } from '@app/profile';
+
  import { Profile, ProfileType } from '@app/profile';
  import Loading from '@app/Loading.svelte';
  import Avatar from "@app/Avatar.svelte";
  import type { Config } from '@app/config';
@@ -25,7 +25,7 @@
  onMount(async () => {
    identifyAddress(address, config).then((t: AddressType) => addressType = t);
    if (resolve && !profile) {
-
      Profile.get(address, config).then(p => profile = p);
+
      Profile.get(address, ProfileType.Minimal, config).then(p => profile = p);
    }
  });
  $: addressLabel = profile?.name ? compact ? parseEnsLabel(profile.name, config) : profile.name : checksumAddress;
modified src/Header.svelte
@@ -8,7 +8,7 @@
  import Logo from '@app/Logo.svelte';
  import Connect from '@app/Connect.svelte';
  import type { Config } from '@app/config';
-
  import { Profile } from "@app/profile";
+
  import { Profile, ProfileType } from "@app/profile";
  import Avatar from "./Avatar.svelte";

  export let session: Session | null;
@@ -154,7 +154,7 @@
        on:mouseout={() => sessionButtonHover = false}
        on:blur={() => sessionButtonHover = false}
      >
-
        {#await Profile.get(address, config)}
+
        {#await Profile.get(address, ProfileType.Minimal, config)}
          <Loading small center />
        {:then profile}
          {#if sessionButtonHover}
modified src/base/orgs/Org.ts
@@ -3,7 +3,7 @@ import type { TransactionResponse } from '@ethersproject/providers';
import type { ContractReceipt } from '@ethersproject/contracts';
import { OperationType } from "@gnosis.pm/safe-core-sdk-types";

-
import { Profile } from '@app/profile';
+
import { Profile, ProfileType } from '@app/profile';
import { assert } from '@app/error';
import * as utils from '@app/utils';
import type { Config } from '@app/config';
@@ -142,8 +142,12 @@ export class Org {
    return projects;
  }

-
  async getProfile(config: Config): Promise<Profile> {
-
    return Org.getProfile(this.address, config);
+
  async getProfile(profileType: ProfileType, config: Config): Promise<Profile> {
+
    return Org.getProfile(this.address, profileType, config);
+
  }
+

+
  static async getProjectProfile(addr: string, config: Config): Promise<Profile> {
+
    return Org.getProfile(addr, ProfileType.Project, config);
  }

  static async getAnchor(orgAddr: string, urn: string, config: Config): Promise<string | null> {
@@ -214,8 +218,8 @@ export class Org {

  // Return the org profile if there is one, otherwise try to get the profile
  // of its owner.
-
  static async getProfile(address: string, config: Config): Promise<Profile> {
-
    const profile = await Profile.get(address, config);
+
  static async getProfile(address: string, profileType: ProfileType, config: Config): Promise<Profile> {
+
    const profile = await Profile.get(address, profileType, config);

    if (profile.ens) { // Orgs only use ENS for profile information.
      return profile;
@@ -223,7 +227,7 @@ export class Org {
    const org = await Org.get(address, config);

    if (org) {
-
      const ownerProfile = await Profile.get(org.owner, config);
+
      const ownerProfile = await Profile.get(org.owner, profileType, config);

      if (ownerProfile.ens || ownerProfile.idx) {
        return ownerProfile;
modified src/base/orgs/View.svelte
@@ -17,7 +17,7 @@

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

  export let address: string;
  export let config: Config;
@@ -133,7 +133,7 @@
{:then org}
  {#if org}
    <main>
-
      {#await org.getProfile(config)}
+
      {#await org.getProfile(ProfileType.Full, config)}
        <div class="centered">
          <Loading center />
        </div>
@@ -230,7 +230,7 @@
          {#if members.length > 0}
            <div class="members">
              {#each members as address}
-
                {#await Profile.get(address, config)}
+
                {#await Profile.get(address, ProfileType.Minimal, config)}
                  <Loading small />
                {:then profile}
                  <div class="member">
modified src/base/projects/Browser.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
  import { navigate } from 'svelte-routing';
  import type { Config } from '@app/config';
+
  import type { Profile } from '@app/profile';
  import * as proj from '@app/project';
  import Loading from '@app/Loading.svelte';
  import Address from '@app/Address.svelte';
@@ -26,7 +27,9 @@
  export let commit: string;
  export let config: Config;
  export let path: string;
-
  export let org = "";
+
  export let org: Profile | null = null;
+

+
  const orgAddress = org?.address;

  // When the component is loaded the first time, the blob is yet to be loaded.
  let state: State = { status: Status.Idle };
@@ -54,7 +57,7 @@
    const blob = await loadBlob(path);
    getBlob = new Promise(resolve => resolve(blob));

-
    navigate(proj.path({ urn, org, commit, path }));
+
    navigate(proj.path({ urn, org: orgAddress, commit, path }));
  };

  const fetchTree = async (path: string) => {
@@ -64,7 +67,7 @@
  // This is reactive to respond to path changes that don't originate from this
  // component, eg. when using the browser's "back" button.
  $: getBlob = loadBlob(path);
-
  $: getAnchor = org ? Org.getAnchor(org, urn, config) : null;
+
  $: getAnchor = orgAddress ? Org.getAnchor(orgAddress, urn, config) : null;
  $: loadingPath = state.status == Status.Loading ? state.path : null;
</script>

@@ -191,18 +194,14 @@
        </div>
      {/if}
      <div class="anchor">
-
        {#if org}
+
        {#if orgAddress}
          {#await getAnchor}
            <Loading small margins />
          {:then anchor}
            {#if anchor === commit}
              <span class="anchor-widget">
                <span class="anchor-label">anchored</span>
-
                {#await Org.getProfile(org, config)}
-
                  <Loading small />
-
                {:then profile}
-
                  <Address address={org} compact resolve noBadge {profile} {config} />
-
                {/await}
+
                <Address address={orgAddress} compact resolve noBadge profile={org} {config} />
              </span>
            {:else}
              <span class="anchor-widget not-anchored">
modified src/base/projects/View.svelte
@@ -6,6 +6,7 @@
  import Modal from '@app/Modal.svelte';
  import Avatar from '@app/Avatar.svelte';
  import { Org } from '@app/base/orgs/Org';
+
  import type { Profile } from '@app/profile';

  import Browser from './Browser.svelte';

@@ -16,16 +17,17 @@
  export let path: string;

  let projectRoot = proj.path({ urn, org, commit });
-
  let getProject = new Promise<string | null>(resolve => {
+
  let getProject = new Promise<Profile | null>(resolve => {
    if (org) {
-
      Org.getProfile(org, config).then(p => resolve(p?.seed || null));
+
      Org.getProjectProfile(org, config).then(p => resolve(p));
    } else {
      resolve(null);
    }
-
  }).then(async (seed) => {
+
  }).then(async (orgProfile) => {
+
    const seed = orgProfile?.seed;
    const cfg = seed ? config.withSeed(seed) : config;
    const info = await proj.getInfo(urn, cfg);
-
    return { project: info, config: cfg };
+
    return { project: info, config: cfg, org: orgProfile };
  });

  const back = () => window.history.back();
@@ -96,7 +98,7 @@
      <div class="urn">{urn}</div>
      <div class="description">{result.project.meta.description}</div>
    </header>
-
    <Browser {urn} {org} {path}
+
    <Browser {urn} org={result.org} {path}
      commit={commit || result.project.head}
      config={result.config} />
  {:catch}
modified src/base/registrations/View.svelte
@@ -46,8 +46,8 @@
      .then(async r => {
        if (r) {
          let reverseRecord = false;
-
          if (r.address) {
-
            reverseRecord = await isReverseRecordSet(r.address, name, config);
+
          if (r.profile.address) {
+
            reverseRecord = await isReverseRecordSet(r.profile.address, name, config);
          }

          fields = [
@@ -62,25 +62,25 @@
                  + "For this name to be correctly associated with the address, "
                  + "a reverse record should be set."
              ),
-
              value: r.address, editable: true },
+
              value: r.profile.address, editable: true },
            { name: "url", label: "URL", validate: "url", placeholder: "https://acme.org",
              description: "A homepage or other URL associated with this name.",
-
              value: r.url,editable: true },
+
              value: r.profile.url,editable: true },
            { name: "avatar", validate: "url", placeholder: "https://acme.org/avatar.png",
              description: "An avatar or square image associated with this name.",
-
              value: r.avatar, editable: true },
+
              value: r.profile.avatar, editable: true },
            { name: "twitter", validate: "handle", placeholder: "Twitter username, eg. 'acme'",
              description: "The Twitter handle associated with this name.",
-
              value: r.twitter, editable: true },
+
              value: r.profile.twitter, editable: true },
            { name: "github", validate: "handle", label: "GitHub", placeholder: "GitHub username, eg. 'acme'",
              description: "The GitHub username associated with this name.",
-
              value: r.github, editable: true },
+
              value: r.profile.github, editable: true },
            { name: "seed.id", label: "Seed ID", placeholder: "hynkyn...3nrzc@seed.acme.org:8887",
              description: "The ID of a Radicle Link node that hosts entities associated with this name.",
-
              value: r.seedId, editable: true },
+
              value: r.profile.seedId, editable: true },
            { name: "seed.api", label: "Seed API", validate: "url", placeholder: "https://seed.acme.org:8888",
              description: "The HTTP address of a node that serves Radicle entities over HTTP.",
-
              value: r.seedApi, editable: true },
+
              value: r.profile.seedApi, editable: true },
          ];
          state = { status: Status.Found, registration: r };
        } else {
modified src/base/registrations/registrar.ts
@@ -10,8 +10,14 @@ import { unixTime } from '@app/utils';
import { assert } from '@app/error';

export interface Registration {
-
  name: string;
  owner: string;
+
  profile: EnsProfile;
+
  resolver: EnsResolver;
+
}
+

+
export interface EnsProfile {
+
  name: string;
+
  owner?: string;
  address: string | null;
  seedId: string | null;
  seedApi: string | null;
@@ -19,7 +25,6 @@ export interface Registration {
  avatar: string | null;
  twitter: string | null;
  github: string | null;
-
  resolver: EnsResolver;
}

export enum State {
@@ -76,19 +81,41 @@ export async function getRegistration(name: string, config: Config): Promise<Reg
    meta.map(r => r.status == "fulfilled" ? r.value : null);

  return {
-
    name,
-
    url,
-
    avatar,
-
    seedId,
-
    seedApi,
    owner,
-
    address,
-
    twitter,
-
    github,
    resolver,
+
    profile: {
+
      name,
+
      url,
+
      avatar,
+
      seedId,
+
      seedApi,
+
      address,
+
      twitter,
+
      github,
+
    },
  };
}

+
export async function getAvatar(name: string, config: Config): Promise<string | null> {
+
  name = name.toLowerCase();
+

+
  const resolver = await config.provider.getResolver(name);
+
  if (! resolver) {
+
    return null;
+
  }
+
  return resolver.getText('avatar');
+
}
+

+
export async function getSeed(name: string, config: Config): Promise<string | null> {
+
  name = name.toLowerCase();
+

+
  const resolver = await config.provider.getResolver(name);
+
  if (! resolver) {
+
    return null;
+
  }
+
  return resolver.getText('eth.radicle.seed.api');
+
}
+

export function registrar(config: Config): ethers.Contract {
  return new ethers.Contract(config.registrar.address, config.abi.registrar, config.provider);
}
modified src/base/users/View.svelte
@@ -3,7 +3,7 @@
  import Icon from '@app/Icon.svelte';
  import Address from '@app/Address.svelte';
  import Avatar from '@app/Avatar.svelte';
-
  import { Profile } from '@app/profile';
+
  import { ProfileType, Profile } from '@app/profile';
  import Loading from '@app/Loading.svelte';
  import { Org } from '@app/base/orgs/Org';
  import Message from '@app/Message.svelte';
@@ -57,7 +57,7 @@
  }
</style>

-
{#await Profile.get(address, config)}
+
{#await Profile.get(address, ProfileType.Full, config)}
  <Loading fadeIn />
{:then profile}
  <main>
modified src/profile.ts
@@ -1,5 +1,5 @@
import { ethers } from "ethers";
-
import type { Registration } from "@app/base/registrations/registrar";
+
import type { EnsProfile } from "@app/base/registrations/registrar";
import type { BasicProfile } from "@ceramicstudio/idx-constants";
import {
  formatCAIP10Address, formatIpfsFile, resolveEnsProfile, resolveIdxProfile, parseUsername,
@@ -7,10 +7,16 @@ import {
import type { Config } from "./config";

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

+
export enum ProfileType {
+
  Full,
+
  Minimal,
+
  Project,
+
}
+

export class Profile {
  profile: IProfile;
  address: string;
@@ -21,7 +27,7 @@ export class Profile {
  }

  // Get the ENS profile
-
  get ens(): Registration | null {
+
  get ens(): EnsProfile | null {
    return this.profile.ens;
  }

@@ -80,7 +86,11 @@ export class Profile {
  }

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

    try {
@@ -91,7 +101,7 @@ export class Profile {

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

@@ -105,16 +115,17 @@ export class Profile {
  }

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

  static async get(
    address: string,
+
    profileType: ProfileType,
    config: Config,
  ): Promise<Profile> {
-
    const profile = await this.lookupAddress(address, config);
+
    const profile = await this.lookupAddress(address, profileType, config);
    return new Profile(...profile);
  }
}
modified src/utils.ts
@@ -6,10 +6,10 @@ 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 { EnsProfile } from "@app/base/registrations/registrar";
+
import { getAvatar, getSeed, getRegistration } from '@app/base/registrations/registrar';
import type { BasicProfile } from "@ceramicstudio/idx-constants";
-

+
import { ProfileType } from '@app/profile';

export enum AddressType {
  Contract,
@@ -246,10 +246,42 @@ export async function resolveIdxProfile(caip10: string, config: Config): Promise
}

// Resolves an ENS profile or return null
-
export async function resolveEnsProfile(address: string, config: Config): Promise<Registration | null> {
+
export async function resolveEnsProfile(address: string, profileType: ProfileType, config: Config): Promise<EnsProfile | null> {
  const name = await config.provider.lookupAddress(address);
  if (name) {
-
    return await getRegistration(name, config);
+
    if (profileType === ProfileType.Full) {
+
      const registration = await getRegistration(name, config);
+
      if (registration) {
+
        return registration.profile;
+
      }
+
    } else if (profileType === ProfileType.Project) {
+
      const avatar = await getAvatar(name, config);
+
      const seedApi = await getSeed(name, config);
+

+
      return {
+
        name,
+
        address,
+
        avatar,
+
        seedApi,
+
        url: null,
+
        seedId: null,
+
        twitter: null,
+
        github: null,
+
      };
+
    } else if (profileType === ProfileType.Minimal) {
+
      const avatar = await getAvatar(name, config);
+

+
      return {
+
        name,
+
        address,
+
        avatar,
+
        url: null,
+
        seedId: null,
+
        seedApi: null,
+
        twitter: null,
+
        github: null,
+
      };
+
    }
  }
  return null;
}