Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add caching
Sebastian Martinez committed 3 years ago
commit 694bb38b8aa9ddc08de9c460ec761861f1239e4b
parent 37ff1436837172b4924652796a395edd93611b09
10 files changed +335 -66
modified package-lock.json
@@ -29,6 +29,7 @@
        "ethers": "^5.5.0",
        "eventemitter3": "4.0.7",
        "events": "^3.3.0",
+
        "lru-cache": "^7.13.2",
        "marked": "^4.0.12",
        "md5": "^2.3.0",
        "multibase": "^4.0.4",
@@ -3723,6 +3724,18 @@
        }
      }
    },
+
    "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": {
+
      "version": "6.0.0",
+
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+
      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+
      "dev": true,
+
      "dependencies": {
+
        "yallist": "^4.0.0"
+
      },
+
      "engines": {
+
        "node": ">=10"
+
      }
+
    },
    "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
      "version": "7.3.7",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
@@ -3868,6 +3881,18 @@
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
+
    "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": {
+
      "version": "6.0.0",
+
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+
      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+
      "dev": true,
+
      "dependencies": {
+
        "yallist": "^4.0.0"
+
      },
+
      "engines": {
+
        "node": ">=10"
+
      }
+
    },
    "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
      "version": "7.3.7",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
@@ -5585,6 +5610,18 @@
        "ieee754": "^1.1.13"
      }
    },
+
    "node_modules/cypress/node_modules/lru-cache": {
+
      "version": "6.0.0",
+
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+
      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+
      "dev": true,
+
      "dependencies": {
+
        "yallist": "^4.0.0"
+
      },
+
      "engines": {
+
        "node": ">=10"
+
      }
+
    },
    "node_modules/cypress/node_modules/semver": {
      "version": "7.3.5",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@@ -6617,6 +6654,18 @@
      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
      "dev": true
    },
+
    "node_modules/eslint/node_modules/lru-cache": {
+
      "version": "6.0.0",
+
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+
      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+
      "dev": true,
+
      "dependencies": {
+
        "yallist": "^4.0.0"
+
      },
+
      "engines": {
+
        "node": ">=10"
+
      }
+
    },
    "node_modules/eslint/node_modules/semver": {
      "version": "7.3.5",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@@ -8841,15 +8890,11 @@
      "integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="
    },
    "node_modules/lru-cache": {
-
      "version": "6.0.0",
-
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-
      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-
      "dev": true,
-
      "dependencies": {
-
        "yallist": "^4.0.0"
-
      },
+
      "version": "7.13.2",
+
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.2.tgz",
+
      "integrity": "sha512-VJL3nIpA79TodY/ctmZEfhASgqekbT574/c4j3jn4bKXbSCnTTCH/KltZyvL2GlV+tGSMtsWyem8DCX7qKTMBA==",
      "engines": {
-
        "node": ">=10"
+
        "node": ">=12"
      }
    },
    "node_modules/lz-string": {
@@ -14733,6 +14778,15 @@
        "tsutils": "^3.21.0"
      },
      "dependencies": {
+
        "lru-cache": {
+
          "version": "6.0.0",
+
          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+
          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+
          "dev": true,
+
          "requires": {
+
            "yallist": "^4.0.0"
+
          }
+
        },
        "semver": {
          "version": "7.3.7",
          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
@@ -14812,6 +14866,15 @@
            "slash": "^3.0.0"
          }
        },
+
        "lru-cache": {
+
          "version": "6.0.0",
+
          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+
          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+
          "dev": true,
+
          "requires": {
+
            "yallist": "^4.0.0"
+
          }
+
        },
        "semver": {
          "version": "7.3.7",
          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
@@ -16190,6 +16253,15 @@
            "ieee754": "^1.1.13"
          }
        },
+
        "lru-cache": {
+
          "version": "6.0.0",
+
          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+
          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+
          "dev": true,
+
          "requires": {
+
            "yallist": "^4.0.0"
+
          }
+
        },
        "semver": {
          "version": "7.3.5",
          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@@ -16853,6 +16925,15 @@
          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
          "dev": true
        },
+
        "lru-cache": {
+
          "version": "6.0.0",
+
          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+
          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+
          "dev": true,
+
          "requires": {
+
            "yallist": "^4.0.0"
+
          }
+
        },
        "semver": {
          "version": "7.3.5",
          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@@ -18505,13 +18586,9 @@
      "integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="
    },
    "lru-cache": {
-
      "version": "6.0.0",
-
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-
      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-
      "dev": true,
-
      "requires": {
-
        "yallist": "^4.0.0"
-
      }
+
      "version": "7.13.2",
+
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.2.tgz",
+
      "integrity": "sha512-VJL3nIpA79TodY/ctmZEfhASgqekbT574/c4j3jn4bKXbSCnTTCH/KltZyvL2GlV+tGSMtsWyem8DCX7qKTMBA=="
    },
    "lz-string": {
      "version": "1.4.4",
modified package.json
@@ -61,6 +61,7 @@
    "ethers": "^5.5.0",
    "eventemitter3": "4.0.7",
    "events": "^3.3.0",
+
    "lru-cache": "^7.13.2",
    "marked": "^4.0.12",
    "md5": "^2.3.0",
    "multibase": "^4.0.4",
modified src/Profile.svelte
@@ -8,7 +8,7 @@
  import SeedAddress from '@app/SeedAddress.svelte';
  import TransferOwnership from '@app/base/orgs/TransferOwnership.svelte';
  import Link from '@app/Link.svelte';
-
  import { Profile, ProfileType } from '@app/profile';
+
  import { getBalance, Profile, ProfileType } from '@app/profile';
  import Loading from '@app/Loading.svelte';
  import * as utils from '@app/utils';
  import { session } from '@app/session';
@@ -46,7 +46,7 @@
    if (addressType === utils.AddressType.Safe) {
      try {
        const tokens = await utils.getTokens(org.owner, config);
-
        const balance = await config.provider.getBalance(org.owner);
+
        const balance = await getBalance(org.owner, config);

        if (! balance.isZero()) {
          // To maintain the format we hardcode the ETH specs.
modified src/base/orgs/Org.ts
@@ -5,6 +5,9 @@ import { OperationType } from "@gnosis.pm/safe-core-sdk-types";

import { assert } from '@app/error';
import * as utils from '@app/utils';
+
import * as cache from "@app/cache";
+
import type { SafeMultisigTransactionListResponse } from '@gnosis.pm/safe-service-client';
+
import type SafeServiceClient from '@gnosis.pm/safe-service-client';
import type { Safe } from '@app/utils';
import type { Config } from '@app/config';
import type { PendingAnchor, Anchor } from '@app/project';
@@ -186,7 +189,9 @@ export class Org {

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

@@ -214,8 +219,8 @@ export class Org {

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

@@ -298,17 +303,10 @@ export class Org {
    addressOrName: string,
    config: Config,
  ): Promise<Org | null> {
-
    const org = new ethers.Contract(
-
      addressOrName,
-
      config.abi.org,
-
      config.provider
-
    );
+
    const org = await getOrgContract(addressOrName, config);

    try {
-
      const [owner, resolved] = await Promise.all([
-
        org.owner(),
-
        org.resolvedAddress,
-
      ]);
+
      const [owner, resolved] = await resolveOrgOwner(org);

      const safe = await utils.getSafe(owner, config);
      // If what is resolved is not the same as the input, it's because we
@@ -391,3 +389,38 @@ export function parseAnchorTx(data: string, config: Config): { id: string; state
  }
  return null;
}
+

+
export const getOrgContract = cache.cached(
+
  async (addressOrName: string, config: Config) => {
+
    return new ethers.Contract(
+
      addressOrName,
+
      config.abi.org,
+
      config.provider
+
    );
+
  },
+
  (addressOrName) => addressOrName
+
);
+

+
export const resolveOrgOwner = cache.cached(
+
  async (org: ethers.Contract) => {
+
    return await Promise.all([
+
      org.owner(),
+
      org.resolvedAddress,
+
    ]);
+
  },
+
  (org) => org.address,
+
);
+

+
export const getPendingProjects = cache.cached(
+
  async (owner: string, client: SafeServiceClient): Promise<SafeMultisigTransactionListResponse> => {
+
    try {
+
      return await client.getPendingTransactions(
+
        ethers.utils.getAddress(owner)
+
      );
+
    } catch (e) {
+
      return { count: 0, results: [] };
+
    }
+
  },
+
  (owner) => owner,
+
  { max: 1000, ttl: 5 * 60 * 1000 } // Cache results for 5 minutes.
+
);
modified src/base/registrations/registrar.ts
@@ -9,6 +9,7 @@ import type { Config } from '@app/config';
import { unixTime } from '@app/utils';
import { assert } from '@app/error';
import { Seed, InvalidSeed } from '@app/base/seeds/Seed';
+
import * as cache from '@app/cache';

export interface Registration {
  profile: EnsProfile;
@@ -63,7 +64,7 @@ export async function getRegistration(name: string, config: Config, resolver?: E
  name = name.toLowerCase();

  if (! resolver) {
-
    resolver = await config.provider.getResolver(name);
+
    resolver = await getResolver(name, config);

    if (! resolver) {
      return null;
@@ -71,17 +72,17 @@ export async function getRegistration(name: string, config: Config, resolver?: E
  }

  const meta = await Promise.allSettled([
-
    resolver.getAddress(),
-
    resolver.getText('avatar'),
-
    resolver.getText('url'),
-
    resolver.getText('eth.radicle.id'),
-
    resolver.getText('eth.radicle.seed.id'),
-
    resolver.getText('eth.radicle.seed.host'),
-
    resolver.getText('eth.radicle.seed.git'),
-
    resolver.getText('eth.radicle.seed.api'),
-
    resolver.getText('eth.radicle.anchors'),
-
    resolver.getText('com.twitter'),
-
    resolver.getText('com.github'),
+
    getAddress(resolver),
+
    getText(resolver, 'avatar'),
+
    getText(resolver, 'url'),
+
    getText(resolver, 'eth.radicle.id'),
+
    getText(resolver, 'eth.radicle.seed.id'),
+
    getText(resolver, 'eth.radicle.seed.host'),
+
    getText(resolver, 'eth.radicle.seed.git'),
+
    getText(resolver, 'eth.radicle.seed.api'),
+
    getText(resolver, 'eth.radicle.anchors'),
+
    getText(resolver, 'com.twitter'),
+
    getText(resolver, 'com.github'),
  ]);

  const [address, avatar, url, id, seedId, seedHost, seedGit, seedApi, anchorsAccount, twitter, github] =
@@ -116,36 +117,36 @@ export async function getRegistration(name: string, config: Config, resolver?: E
export async function getAvatar(name: string, config: Config, resolver?: EnsResolver | null): Promise<string | null> {
  name = name.toLowerCase();

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

export async function getAnchorsAccount(name: string, config: Config, resolver?: EnsResolver | null): Promise<string | null> {
  name = name.toLowerCase();

-
  resolver = resolver ?? await config.provider.getResolver(name);
+
  resolver = resolver ?? await getResolver(name, config);
  if (! resolver) {
    return null;
  }
-
  return resolver.getText('eth.radicle.anchors');
+
  return getText(resolver, 'eth.radicle.anchors');
}

export async function getSeed(name: string, config: Config, resolver?: EnsResolver | null): Promise<Seed | InvalidSeed | null> {
  name = name.toLowerCase();

-
  resolver = resolver ?? await config.provider.getResolver(name);
+
  resolver = resolver ?? await getResolver(name, config);
  if (! resolver) {
    return null;
  }

  const [id, host, git, api] = await Promise.all([
-
    resolver.getText('eth.radicle.seed.id'),
-
    resolver.getText('eth.radicle.seed.host'),
-
    resolver.getText('eth.radicle.seed.git'),
-
    resolver.getText('eth.radicle.seed.api'),
+
    getText(resolver, 'eth.radicle.seed.id'),
+
    getText(resolver, 'eth.radicle.seed.host'),
+
    getText(resolver, 'eth.radicle.seed.git'),
+
    getText(resolver, 'eth.radicle.seed.api'),
  ]);

  if (! host || ! id) {
@@ -344,3 +345,27 @@ export async function getOwner(name: string, config: Config): Promise<string> {

  return owner;
}
+

+
export const getResolver = cache.cached(
+
  async (name: string, config: Config) => {
+
    return await config.provider.getResolver(name);
+
  },
+
  (name) => name,
+
  { max: 1000 }
+
);
+

+
export const getText = cache.cached(
+
  async (resolver: EnsResolver, key: string) => {
+
    return await resolver.getText(key);
+
  },
+
  (resolver, key) => `${resolver.name} ${key}`,
+
  { max: 1000 }
+
);
+

+
export const getAddress = cache.cached(
+
  async (resolver: EnsResolver) => {
+
    return await resolver.getAddress();
+
  },
+
  (resolver) => resolver.name,
+
  { max: 1000 }
+
);
added src/base/resolver/List.svelte
@@ -0,0 +1,45 @@
+
<script lang="ts">
+
  import Modal from '@app/Modal.svelte';
+
  import Address from "@app/Address.svelte";
+
  import type { Config } from '@app/config';
+
  import { Profile } from '@app/profile';
+
  import Loading from '@app/Loading.svelte';
+

+
  export let config: Config;
+

+
  const { query } = window.history.state;
+
  const back = () => window.history.back();
+
</script>
+

+
<style>
+
  .list {
+
    display: flex;
+
    flex-direction: column;
+
    align-items: center;
+
  }
+
</style>
+

+
  <Modal subtle>
+
    <span slot="title">️🔍</span>
+
    <span slot="subtitle">
+
      <p class="highlight text-medium"><strong>Multiple names found for {query}</strong></p>
+
    </span>
+
    <span slot="body">
+
      <div class="list">
+
        {#await Profile.getMulti([`${query}.${config.registrar.domain}`, `${query}.eth`], config)}
+
          <Loading center />
+
        {:then profiles}
+
          {#each profiles as profile}
+
            {#if profile}
+
              <Address address={profile.address} profile={profile} {config} resolve />
+
            {/if}
+
          {/each}
+
        {/await}
+
      </div>
+
    </span>
+
    <span slot="actions">
+
      <button on:click={back}>
+
        Back
+
      </button>
+
    </span>
+
  </Modal>
added src/cache.test.ts
@@ -0,0 +1,13 @@
+
import { cached } from "@app/cache";
+
import { expect, test, vi } from "vitest";
+

+
test("it caches undefined return values", async () => {
+
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+
  const inner = vi.fn(async (_: string) => undefined);
+
  const memoized = cached(inner, key => key);
+

+
  expect(await memoized("a")).toBe(undefined);
+
  expect(await memoized("a")).toBe(undefined);
+

+
  expect(inner).toHaveBeenCalledTimes(1);
+
});
added src/cache.ts
@@ -0,0 +1,25 @@
+
import LruCache from "lru-cache";
+

+
// Creates a function that memoizes its result using an LRU cache.
+
//
+
// The cache key is a string created from the arguments using
+
// `makeKey`.
+
export function cached<Args extends unknown[], V>(
+
  f: (...args: Args) => Promise<V>,
+
  makeKey: (...args: Args) => string,
+
  options?: LruCache.Options<string, { value: V }>
+
): (...args: Args) => Promise<V> {
+
  const cache = new LruCache(options || { max: 500 });
+
  return async function (...args: Args): Promise<V> {
+
    const key = makeKey(...args);
+
    const cached = cache.get(key);
+

+
    if (cached === undefined) {
+
      const value = await f(...args);
+
      cache.set(key, { value });
+
      return value;
+
    } else {
+
      return cached.value;
+
    }
+
  };
+
}
modified src/profile.ts
@@ -5,6 +5,7 @@ import {
  resolveIdxProfile, parseUsername, AddressType, identifyAddress
} from "@app/utils";
import type { Config } from "@app/config";
+
import { cached } from "@app/cache";
import type { Seed, InvalidSeed } from "@app/base/seeds/Seed";
import { Org } from "@app/base/orgs/Org";
import { NotFoundError } from "@app/error";
@@ -212,9 +213,9 @@ export class Profile {
    } else if (isAddress(addressOrName)) {
      const address = addressOrName.toLowerCase();

-
      type = await identifyAddress(addressOrName, config);
+
      type = await identifyAddress(address, config);
      if (type === AddressType.Org) {
-
        org = await Org.get(addressOrName, config);
+
        org = await Org.get(address, config);
      }

      try {
@@ -239,7 +240,9 @@ export class Profile {
  }

  static async getMulti(addressesOrNames: string[], config: Config): Promise<Profile[]> {
-
    const profilePromises = addressesOrNames.map(addressOrName => this.lookupProfile(addressOrName, ProfileType.Minimal, config));
+
    const profilePromises = addressesOrNames.map(
+
      addressOrName => this.lookupProfile(addressOrName, ProfileType.Minimal, config)
+
    );
    const profiles = await Promise.all(profilePromises);
    return profiles.map(profile => { return new Profile(profile); });
  }
@@ -253,3 +256,11 @@ export class Profile {
    return new Profile(profile);
  }
}
+

+
export const getBalance = cached(
+
  async (address: string, config: Config) => {
+
    return await config.provider.getBalance(address);
+
  },
+
  (address) => address,
+
  { max: 1000 }
+
);
modified src/utils.ts
@@ -9,12 +9,13 @@ import type { SafeSignature } from "@gnosis.pm/safe-core-sdk-types";
import type { Config } from '@app/config';
import config from "@app/config.json";
import { assert } from '@app/error';
-
import type { EnsProfile } from "@app/base/registrations/registrar";
+
import { EnsProfile, getAddress, getResolver } from "@app/base/registrations/registrar";
import { getAvatar, getSeed, getAnchorsAccount, getRegistration } from '@app/base/registrations/registrar';
import type { BasicProfile } from '@datamodels/identity-profile-basic';
import { ProfileType } from '@app/profile';
import { parseUnits } from "@ethersproject/units";
import { GetSafe } from "@app/base/orgs/Org";
+
import * as cache from "@app/cache";

export enum AddressType {
  Contract,
@@ -58,7 +59,7 @@ export type State =
  | { status: Status.Failed; error: string };

export async function isReverseRecordSet(address: string, domain: string, config: Config): Promise<boolean> {
-
  const name = await config.provider.lookupAddress(address);
+
  const name = await lookupAddress(address, config);
  return name === domain;
}

@@ -337,7 +338,7 @@ export function safeLink(addr: string, config: Config): string {
}

// Query a subgraph.
-
export async function querySubgraph(
+
export async function querySubgraphWithRetry(
  url: string,
  query: string,
  variables: Record<string, any>,
@@ -358,13 +359,19 @@ export async function querySubgraph(
  if (json.errors) {
    console.error("querySubgraph:", json.errors);

-
    if (retries > 0) querySubgraph(url, query, variables, retries - 1);
+
    if (retries > 0) querySubgraphWithRetry(url, query, variables, retries - 1);
    else return null;
  }

  return json.data;
}

+
export const querySubgraph = cache.cached(
+
  querySubgraphWithRetry,
+
  (url: string, query: string, variables: Record<string, any>) => JSON.stringify({ url, query, variables }),
+
  { max: 500, ttl: 5 * 60 * 1000 } // Cache results for 5 minutes.
+
);
+

// Format a name.
export function formatName(input: string, config: Config): string {
  return parseEnsLabel(input, config);
@@ -417,7 +424,7 @@ export async function identifyAddress(address: string, config: Config): Promise<
    return AddressType.Safe;
  }

-
  const code = await config.provider.getCode(address);
+
  const code = await getCode(address, config);
  const bytes = ethers.utils.arrayify(code);

  if (bytes.length > 0) {
@@ -436,18 +443,26 @@ export async function resolveLabel(label: string | undefined, config: Config): P
}

// Resolves an IDX profile or return null
-
export async function resolveIdxProfile(caip10: string, config: Config): Promise<BasicProfile | null> {
-
  return config.ceramic.client.get("basicProfile", caip10);
-
}
+
export const resolveIdxProfile = cache.cached(
+
  async (caip10: string, config: Config): Promise<BasicProfile | null> => {
+
    try {
+
      return await config.ceramic.client.get("basicProfile", caip10);
+
    } catch (e) {
+
      return null;
+
    }
+
  },
+
  (caip10: string) => caip10,
+
  { max: 500, ttl: 30 * 60 * 1000 } // Cache results for 30 minutes.
+
);

// Resolves an ENS profile or return null
export async function resolveEnsProfile(addressOrName: string, profileType: ProfileType, config: Config): Promise<EnsProfile | null> {
  const name = ethers.utils.isAddress(addressOrName)
-
    ? await config.provider.lookupAddress(addressOrName)
+
    ? await lookupAddress(addressOrName, config)
    : addressOrName;

  if (name) {
-
    const resolver = await config.provider.getResolver(name);
+
    const resolver = await getResolver(name, config);
    if (! resolver) {
      return null;
    }
@@ -463,7 +478,7 @@ export async function resolveEnsProfile(addressOrName: string, profileType: Prof
      ];

      if (addressOrName === name) {
-
        promises.push(resolver.getAddress());
+
        promises.push(getAddress(resolver));
      } else {
        promises.push(Promise.resolve(addressOrName));
      }
@@ -519,7 +534,7 @@ export async function getSafe(address: string, config: Config): Promise<Safe | n

// Get token balances for an address.
export async function getTokens(address: string, config: Config): Promise<Array<Token>> {
-
  const userBalances = await config.provider.send("alchemy_getTokenBalances", [address, "DEFAULT_TOKENS"]);
+
  const userBalances = await getRpcMethod("alchemy_getTokenBalances", [address, "DEFAULT_TOKENS"], config);
  const balances = userBalances.tokenBalances.filter((token: any) => {
    // alchemy_getTokenBalances sometimes returns 0x and this does not work well with ethers.BigNumber
    if (token.tokenBalance !== "0x") {
@@ -528,13 +543,21 @@ export async function getTokens(address: string, config: Config): Promise<Array<
      }
    }
  }).map(async (token: any) => {
-
    const tokenMetaData = await config.provider.send("alchemy_getTokenMetadata", [token.contractAddress]);
+
    const tokenMetaData = await getRpcMethod("alchemy_getTokenMetadata", [token.contractAddress], config);
    return { ...tokenMetaData, balance: BigNumber.from(token.tokenBalance) };
  });

  return Promise.all(balances);
}

+
export const getRpcMethod = cache.cached(
+
  async (method: string, props: string[], config: Config) => {
+
    return await config.provider.send(method, props);
+
  },
+
  (method, props) => JSON.stringify([method, props]),
+
  { ttl: 2 * 60 * 1000 }
+
);
+

// Check whether the given path has a markdown file extension.
export function isMarkdownPath(path: string): boolean {
  return /\.(md|mkd|markdown)$/i.test(path);
@@ -662,3 +685,19 @@ export class EthSignSignature {
    return '';
  }
}
+

+
export const getCode = cache.cached(
+
  async (address: string, config: Config) => {
+
    return await config.provider.getCode(address);
+
  },
+
  (address) => address,
+
  { max: 1000 }
+
);
+

+
export const lookupAddress = cache.cached(
+
  async (address: string, config: Config) => {
+
    return await config.provider.lookupAddress(address);
+
  },
+
  (address) => address,
+
  { max: 1000 }
+
);