Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Clean up config
Rūdolfs Ošiņš committed 3 years ago
commit ab59f11727093ff195b51c8208991a54abc951a5
parent d792c62c92bca75ffdbb30ca96e19d1ee279b4a1
60 files changed +854 -872
modified CONTRIBUTING.md
@@ -6,9 +6,6 @@ simple guidelines.
* Make sure you run `npx eslint .` to check for code formatting problems.
  You can use the `--fix` flag to automatically fix any issues the linter catches.
* Before adding any code dependencies, check with the maintainers if this is okay.
-
* When using configuration parameters, eg. API URLs, API tokens or contract addresses,
-
  add them to `config.json` and pass the `Config` object to functions that require
-
  access to this configuraiton.
* Write properly formatted comments: they should be english sentences, eg:

      // Return the current UNIX time.
modified src/Address.svelte
@@ -12,10 +12,10 @@
  import { Profile, ProfileType } from "@app/profile";
  import Avatar from "@app/Avatar.svelte";
  import Badge from "@app/Badge.svelte";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";

  export let address: string;
-
  export let config: Config;
+
  export let wallet: Wallet;
  export let resolve = false;
  export let noBadge = false;
  export let noAvatar = false;
@@ -32,12 +32,12 @@

  onMount(async () => {
    if (!profile) {
-
      identifyAddress(address, config).then(
+
      identifyAddress(address, wallet).then(
        (t: AddressType) => (addressType = t),
      );

      if (resolve) {
-
        Profile.get(address, ProfileType.Minimal, config).then(
+
        Profile.get(address, ProfileType.Minimal, wallet).then(
          p => (profile = p),
        );
      }
@@ -49,7 +49,7 @@
  $: addressLabel =
    resolve && profile?.name
      ? compact
-
        ? parseEnsLabel(profile.name, config)
+
        ? parseEnsLabel(profile.name, wallet)
        : profile.name
      : checksumAddress;
  $: checksumAddress = compact
@@ -100,7 +100,7 @@
        <Badge variant="foreground">org</Badge>
      {/if}
    {:else if addressType === AddressType.Contract}
-
      <a href={explorerLink(address, config)} target="_blank" rel="noreferrer">
+
      <a href={explorerLink(address, wallet)} target="_blank" rel="noreferrer">
        {addressLabel}
      </a>
      {#if !noBadge}
@@ -110,7 +110,7 @@
      <a use:link href={`/${nameOrAddress}`}>{addressLabel}</a>
    {:else}
      <!-- While we're waiting to find out what address type it is -->
-
      <a href={explorerLink(address, config)} target="_blank" rel="noreferrer">
+
      <a href={explorerLink(address, wallet)} target="_blank" rel="noreferrer">
        {addressLabel}
      </a>
    {/if}
modified src/App.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import { Router, Route } from "svelte-routing";
-
  import { getConfig } from "@app/config";
+
  import { getWallet } from "@app/wallet";
  import { Connection, state, session } from "@app/session";

  import Home from "@app/base/home/Index.svelte";
@@ -15,19 +15,19 @@
  import Modal from "@app/Modal.svelte";
  import ColorPalette from "./ColorPalette.svelte";

-
  const loadConfig = getConfig().then(async cfg => {
+
  const loadWallet = getWallet().then(async wallet => {
    if ($state.connection === Connection.Connected) {
-
      state.refreshBalance(cfg);
+
      state.refreshBalance(wallet);
    } else if ($state.connection === Connection.Disconnected) {
      // Update the session state if we're already connected to WalletConnect
      // from a previous session.
-
      if (cfg.walletConnect.client.connected) {
-
        await state.connectWalletConnect(cfg);
-
      } else if (cfg.metamask.connected) {
-
        await state.connectMetamask(cfg);
+
      if (wallet.walletConnect.client.connected) {
+
        await state.connectWalletConnect(wallet);
+
      } else if (wallet.metamask.connected) {
+
        await state.connectMetamask(wallet);
      }
    }
-
    return cfg;
+
    return wallet;
  });

  function handleKeydown(event: KeyboardEvent) {
@@ -73,29 +73,29 @@
</svelte:head>

<div class="app">
-
  {#await loadConfig}
+
  {#await loadWallet}
    <!-- Loading wallet -->
    <div class="wrapper">
      <Loading center />
    </div>
-
  {:then config}
+
  {:then wallet}
    <ColorPalette />
-
    <Header session={$session} {config} />
+
    <Header session={$session} {wallet} />
    <div class="wrapper">
      <Router>
        <Route path="/">
-
          <Home {config} />
+
          <Home />
        </Route>
        <Route path="vesting">
-
          <Vesting {config} session={$session} />
+
          <Vesting {wallet} session={$session} />
        </Route>
-
        <Registrations {config} session={$session} />
-
        <Seeds {config} session={$session} />
-
        <Faucet {config} />
+
        <Registrations {wallet} session={$session} />
+
        <Seeds {wallet} session={$session} />
+
        <Faucet {wallet} />
        <Route path="/:addressOrName" let:params>
-
          <Profile addressOrName={params.addressOrName} {config} />
+
          <Profile addressOrName={params.addressOrName} {wallet} />
        </Route>
-
        <Projects {config} />
+
        <Projects {wallet} />
      </Router>
    </div>
  {:catch err}
modified src/Authorship.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import { formatRadicleUrn, formatTimestamp } from "@app/utils";
  import Address from "@app/Address.svelte";
  import { Profile, ProfileType } from "@app/profile";
@@ -10,7 +10,7 @@
  export let author: Author;
  export let timestamp: number;
  export let caption: string;
-
  export let config: Config;
+
  export let wallet: Wallet;
  export let profile: Profile | null = null;

  onMount(async () => {
@@ -18,7 +18,7 @@
      profile = await Profile.get(
        author.profile.ens.name,
        ProfileType.Minimal,
-
        config,
+
        wallet,
      );
    }
  });
@@ -52,7 +52,7 @@
      noBadge
      compact
      {noAvatar}
-
      {config}
+
      {wallet}
      {profile}
      address={profile.address} />
  {:else if author.profile}
modified src/Comment.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import { onMount } from "svelte";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { Comment, Thread } from "@app/issue";
  import Avatar from "@app/Avatar.svelte";
  import Markdown from "@app/Markdown.svelte";
@@ -12,7 +12,7 @@
  import Reactions from "@app/Reactions.svelte";

  export let comment: Comment | Thread;
-
  export let config: Config;
+
  export let wallet: Wallet;
  export let caption = "left a comment";
  export let getImage: (path: string) => Promise<Blob>;

@@ -23,7 +23,7 @@
      profile = await Profile.get(
        comment.author.profile.ens.name,
        ProfileType.Minimal,
-
        config,
+
        wallet,
      );
    }
  });
@@ -83,7 +83,7 @@
    <div class="card-header">
      <Authorship
        noAvatar
-
        {config}
+
        {wallet}
        {caption}
        {profile}
        author={comment.author}
modified src/Connect.svelte
@@ -3,27 +3,27 @@
  import { Connection, state } from "@app/session";
  import type { Err } from "@app/error";
  import ErrorModal from "@app/ErrorModal.svelte";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import ConnectWallet from "@app/components/Modal/ConnectWallet.svelte";
  import Button from "@app/Button.svelte";

  export let caption = "Connect";
-
  export let config: Config;
+
  export let wallet: Wallet;
  export let buttonVariant: "foreground" | "primary";

  let error: Err | null = null;

  const onModalClose = () => {
-
    const wcs = get(config.walletConnect.state);
+
    const wcs = get(wallet.walletConnect.state);

    if (wcs.state === "open") {
-
      config.walletConnect.state.set({ state: "close" });
+
      wallet.walletConnect.state.set({ state: "close" });
      wcs.onClose();
    }
  };
  const onConnect = async () => {
    try {
-
      await state.connectWalletConnect(config);
+
      await state.connectWalletConnect(wallet);
    } catch (e: any) {
      walletConnectState.set({ state: "close" });
      error = e;
@@ -31,7 +31,7 @@
  };

  $: connecting = $state.connection === Connection.Connecting;
-
  $: walletConnectState = config.walletConnect.state;
+
  $: walletConnectState = wallet.walletConnect.state;
</script>

<Button
@@ -48,7 +48,7 @@

{#if $walletConnectState.state === "open"}
  <ConnectWallet
-
    {config}
+
    {wallet}
    uri={$walletConnectState.uri}
    on:close={onModalClose} />
{:else if error}
modified src/Form.svelte
@@ -44,7 +44,7 @@
</script>

<script lang="ts">
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";

  import cloneDeep from "lodash/cloneDeep";
  import { link } from "svelte-routing";
@@ -65,7 +65,7 @@
  export let fields: Field[];
  export let editable = false;
  export let disabled = false;
-
  export let config: Config;
+
  export let wallet: Wallet;

  let formFields = cloneDeep(fields);
  let hasErrors = false;
@@ -219,14 +219,14 @@
                <Address
                  resolve={field.resolve ?? false}
                  address={field.value}
-
                  {config} />
+
                  {wallet} />
              </div>
              <div class="layout-mobile-inline">
                <Address
                  compact
                  resolve={field.resolve ?? false}
                  address={field.value}
-
                  {config} />
+
                  {wallet} />
              </div>
            {:else if field.url}
              <div>
modified src/Header.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { ProjectsAndProfiles } from "@app/Search.svelte";
  import type { Session } from "@app/session";

@@ -23,7 +23,7 @@
  import { formatAddress, formatBalance } from "@app/utils";

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

  let query: string;
  let results: ProjectsAndProfiles | null = null;
@@ -141,7 +141,7 @@
    <a use:link href="/" class="logo"><Logo /></a>
    <div class="search">
      <Search
-
        {config}
+
        {wallet}
        on:search={e => {
          ({ query, results } = e.detail);
        }} />
@@ -152,7 +152,7 @@
          <Floating>
            <span slot="toggle">Seeds</span>
            <svelte:fragment slot="modal">
-
              <SeedDropdown seeds={session.siwe} {config} />
+
              <SeedDropdown seeds={session.siwe} />
            </svelte:fragment>
          </Floating>
        </span>
@@ -161,11 +161,11 @@
  </div>

  <div class="right">
-
    {#if config && config.network.name === "goerli"}
+
    {#if wallet && wallet.network.name === "goerli"}
      <a use:link href="/faucet">
        <span class="network">Goerli</span>
      </a>
-
    {:else if config && config.network.name === "homestead"}
+
    {:else if wallet && wallet.network.name === "homestead"}
      <!-- Don't show anything -->
    {:else}
      <span class="network unavailable">No Network</span>
@@ -185,12 +185,12 @@
      <Button
        style="width: 10rem; white-space: nowrap;"
        variant="foreground"
-
        on:click={() => disconnectWallet(config)}
+
        on:click={() => disconnectWallet(wallet)}
        on:mouseover={() => (sessionButtonHover = true)}
        on:focus={() => (sessionButtonHover = true)}
        on:mouseout={() => (sessionButtonHover = false)}
        on:blur={() => (sessionButtonHover = false)}>
-
        {#await Profile.get(address, ProfileType.Minimal, config)}
+
        {#await Profile.get(address, ProfileType.Minimal, wallet)}
          <Loading small center />
        {:then profile}
          {#if sessionButtonHover}
@@ -203,9 +203,9 @@
          {/if}
        {/await}
      </Button>
-
    {:else if config}
+
    {:else if wallet}
      <span class="connect">
-
        <Connect buttonVariant="foreground" {config} />
+
        <Connect buttonVariant="foreground" {wallet} />
      </span>
    {/if}
    <ThemeToggle />
@@ -221,7 +221,7 @@
          <div class="modal">
            <div style="padding-bottom: 1rem;">
              <Search
-
                {config}
+
                {wallet}
                on:finished={() => {
                  closeFocused();
                }}
@@ -245,7 +245,7 @@

  {#if results}
    <SearchResults
-
      {config}
+
      {wallet}
      {results}
      {query}
      on:close={() => {
modified src/Profile.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import type { SvelteComponent } from "svelte";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { Seed, Stats } from "@app/base/seeds/Seed";
  import type { ProjectInfo } from "@app/project";
  import Address from "@app/Address.svelte";
@@ -21,8 +21,9 @@
  import Async from "@app/Async.svelte";
  import Badge from "@app/Badge.svelte";
  import Button from "@app/Button.svelte";
+
  import { defaultLinkPort } from "@app/base/seeds/Seed";

-
  export let config: Config;
+
  export let wallet: Wallet;
  export let addressOrName: string;
  export let action: string | null = null;

@@ -125,7 +126,7 @@
  <title>{addressOrName}</title>
</svelte:head>

-
{#await Profile.get(addressOrName, ProfileType.Full, config)}
+
{#await Profile.get(addressOrName, ProfileType.Full, wallet)}
  <div class="layout-centered">
    <Loading center />
  </div>
@@ -141,12 +142,12 @@
        <span class="title txt-title">
          <span class="txt-bold layout-desktop">
            {profile.name
-
              ? utils.formatName(profile.name, config)
+
              ? utils.formatName(profile.name, wallet)
              : profile.address}
          </span>
          <span class="txt-bold layout-mobile">
            {profile.name
-
              ? utils.formatName(profile.name, config)
+
              ? utils.formatName(profile.name, wallet)
              : utils.formatAddress(profile.address)}
          </span>
          {#if profile.name && profile.org}
@@ -187,25 +188,25 @@
      <!-- Seed Address -->
      {#if profile.seed && profile.seed.valid}
        <div class="txt-highlight">Seed</div>
-
        <SeedAddress seed={profile.seed} port={config.seed.link.port} />
+
        <SeedAddress seed={profile.seed} port={defaultLinkPort} />
      {/if}
      <!-- Address -->
      <div class="txt-highlight">Address</div>
      <div class="layout-desktop">
-
        <Address {config} {profile} address={profile.address} />
+
        <Address {wallet} {profile} address={profile.address} />
      </div>
      <div class="layout-mobile">
-
        <Address compact {config} {profile} address={profile.address} />
+
        <Address compact {wallet} {profile} address={profile.address} />
      </div>
      <div class="layout-desktop" />
      <!-- Owner -->
      {#if profile.org}
        <div class="txt-highlight">Owner</div>
        <div class="layout-desktop">
-
          <Address resolve {config} address={profile.org.owner} />
+
          <Address resolve {wallet} address={profile.org.owner} />
        </div>
        <div class="layout-mobile">
-
          <Address compact resolve {config} address={profile.org.owner} />
+
          <Address compact resolve {wallet} address={profile.org.owner} />
        </div>
        <div class="layout-desktop" />
      {/if}
@@ -258,7 +259,7 @@
  <svelte:component
    this={setNameForm}
    entity={new User(profile.address)}
-
    {config}
+
    {wallet}
    on:close={() => (setNameForm = null)} />
{:catch err}
  {#if err instanceof NotFoundError}
modified src/Review.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import { onMount } from "svelte";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { Review } from "@app/patch";
  import { formatVerdict } from "@app/patch";
  import type { Blob } from "@app/project";
@@ -10,7 +10,7 @@
  import Comment from "@app/Comment.svelte";

  export let review: Review;
-
  export let config: Config;
+
  export let wallet: Wallet;
  export let getImage: (path: string) => Promise<Blob>;

  let profile: Profile | null = null;
@@ -20,7 +20,7 @@
      profile = await Profile.get(
        review.author.profile.ens.name,
        ProfileType.Minimal,
-
        config,
+
        wallet,
      );
    }
  });
@@ -34,14 +34,14 @@

{#if review.comment.body}
  <Comment
-
    {config}
+
    {wallet}
    {getImage}
    comment={review.comment}
    caption={formatVerdict(review.verdict)} />
{:else}
  <div>
    <Authorship
-
      {config}
+
      {wallet}
      {profile}
      author={review.author}
      timestamp={review.timestamp}
modified src/Search.svelte
@@ -7,6 +7,7 @@
  import * as utils from "@app/utils";
  import { Profile } from "@app/profile";
  import { Project } from "@app/project";
+
  import config from "@app/config.json";

  export interface ProjectsAndProfiles {
    projects: { info: ProjectInfo; seed: Host }[];
@@ -22,7 +23,7 @@

  async function searchProjectsAndProfiles(
    query: string,
-
    config: Config,
+
    wallet: Wallet,
  ): Promise<SearchResult> {
    try {
      // The query is a plain Ethereum address.
@@ -30,9 +31,9 @@
        return { type: "singleProfile", id: query };
      }

-
      const projectOnSeeds = Object.keys(config.seeds.pinned).map(seed => ({
+
      const projectOnSeeds = config.seeds.pinned.map(seed => ({
        nameOrUrn: query,
-
        seed,
+
        seed: seed.host,
      }));

      // The query is a radicle project URN.
@@ -69,15 +70,15 @@

      try {
        let params: string[];
-
        if (utils.isENSName(normalizedQuery, config)) {
+
        if (utils.isENSName(normalizedQuery, wallet)) {
          params = [normalizedQuery];
        } else {
          params = [
-
            `${normalizedQuery}.${config.registrar.domain}`,
+
            `${normalizedQuery}.${wallet.registrar.domain}`,
            `${normalizedQuery}.eth`,
          ];
        }
-
        const profiles = await Profile.getMulti(params, config);
+
        const profiles = await Profile.getMulti(params, wallet);
        projectsAndProfiles.profiles.push(...profiles);
      } catch {
        // TODO: collect errors and forward to user.
@@ -122,7 +123,7 @@
</script>

<script lang="ts" strictEvents>
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";

  import debounce from "lodash/debounce";
  import { createEventDispatcher } from "svelte";
@@ -131,7 +132,7 @@
  import TextInput from "@app/TextInput.svelte";
  import { unreachable } from "@app/utils";

-
  export let config: Config;
+
  export let wallet: Wallet;

  const dispatch = createEventDispatcher<{
    finished: boolean;
@@ -155,7 +156,7 @@
    loading = true;

    const query = input;
-
    const searchResult = await searchProjectsAndProfiles(input, config);
+
    const searchResult = await searchProjectsAndProfiles(input, wallet);

    if (searchResult.type === "nothing") {
      shake();
modified src/SeedAddress.spec.ts
@@ -1,53 +1,42 @@
import { Seed } from "@app/base/seeds/Seed";
-
import { getConfig } from "@app/config";

import SeedAddress from "./SeedAddress.svelte";

describe("SeedAddress", () => {
  it("shows the seed emoji and seed host", () => {
-
    getConfig().then(cfg => {
-
      const seed = new Seed(
-
        {
-
          host: "seed.cloudhead.io",
-
          id: "hydkkkf5ksbe5fuszdhpqhytu3q36gwagj874wxwpo5a8ti8coygh1",
-
        },
-
        cfg,
-
      );
+
    const seed = new Seed({
+
      host: "seed.cloudhead.io",
+
      id: "hydkkkf5ksbe5fuszdhpqhytu3q36gwagj874wxwpo5a8ti8coygh1",
+
    });

-
      cy.mount(SeedAddress, {
-
        props: {
-
          seed,
-
          port: 8776,
-
        },
-
      });
-
      cy.get("span.seed-icon").should("have.text", "🐱");
-
      cy.contains("seed.cloudhead.io")
-
        .should("have.attr", "href", "/seeds/seed.cloudhead.io")
-
        .should("be.visible");
+
    cy.mount(SeedAddress, {
+
      props: {
+
        seed,
+
        port: 8776,
+
      },
    });
+
    cy.get("span.seed-icon").should("have.text", "🌱");
+
    cy.contains("seed.cloudhead.io")
+
      .should("have.attr", "href", "/seeds/seed.cloudhead.io")
+
      .should("be.visible");
  });

  it("shows the full seed id", () => {
-
    getConfig().then(cfg => {
-
      const seed = new Seed(
-
        {
-
          host: "seed.cloudhead.io",
-
          id: "hydkkkf5ksbe5fuszdhpqhytu3q36gwagj874wxwpo5a8ti8coygh1",
-
        },
-
        cfg,
-
      );
-
      cy.mount(SeedAddress, {
-
        props: {
-
          seed,
-
          port: 8776,
-
          full: true,
-
        },
-
      });
-
      cy.get("span.seed-icon").should("have.text", "🐱");
-
      cy.get("body")
-
        .contains("hydkkk…coygh1@seed.cloudhead.io")
-
        .should("be.visible");
-
      cy.get("body").contains(":8776").should("be.visible");
+
    const seed = new Seed({
+
      host: "seed.cloudhead.io",
+
      id: "hydkkkf5ksbe5fuszdhpqhytu3q36gwagj874wxwpo5a8ti8coygh1",
+
    });
+
    cy.mount(SeedAddress, {
+
      props: {
+
        seed,
+
        port: 8776,
+
        full: true,
+
      },
    });
+
    cy.get("span.seed-icon").should("have.text", "🌱");
+
    cy.get("body")
+
      .contains("hydkkk…coygh1@seed.cloudhead.io")
+
      .should("be.visible");
+
    cy.get("body").contains(":8776").should("be.visible");
  });
});
modified src/SeedDropdown.svelte
@@ -1,19 +1,17 @@
<script lang="ts">
  import { navigate } from "svelte-routing";
  import { Seed } from "@app/base/seeds/Seed";
-
  import type { Config } from "@app/config";
  import Dropdown from "@app/Dropdown.svelte";
  import type { SeedSession } from "@app/siwe";
  import { closeFocused } from "@app/Floating.svelte";

  export let seeds: { [key: string]: SeedSession };
-
  export let config: Config;

  // When a user signs into a new seed we want to update the seed listing
  $: formatSeeds = async () => {
    return await Promise.all(
      Object.values(seeds).map(async session => {
-
        const seed = await Seed.lookup(session.domain, config);
+
        const seed = await Seed.lookup(session.domain);
        const key = `${seed.emoji} ${seed.host}`;

        return {
modified src/SiweConnect.svelte
@@ -1,14 +1,14 @@
<script lang="ts">
  import Avatar from "@app/Avatar.svelte";
  import type { Seed } from "@app/base/seeds/Seed";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import { signInWithEthereum } from "@app/siwe";
  import Loading from "@app/Loading.svelte";
  import { Connection } from "@app/session";
  import Button from "@app/Button.svelte";

  export let seed: Seed;
-
  export let config: Config;
+
  export let wallet: Wallet;
  export let caption = "Sign in";
  export let tooltip = "";
  export let disabled = false;
@@ -33,7 +33,7 @@
  on:click={async () => {
    connection = Connection.Connecting;
    try {
-
      await signInWithEthereum(seed, config);
+
      await signInWithEthereum(seed, wallet);
    } catch (e) {
      console.error("Sign in", e);
      connection = Connection.Disconnected;
modified src/base/faucet/Index.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";

  import { session } from "@app/session";
  import { setOpenGraphMetaTag, toWei, capitalize } from "@app/utils";
@@ -13,7 +13,7 @@
  import Button from "@app/Button.svelte";
  import TextInput from "@app/TextInput.svelte";

-
  export let config: Config;
+
  export let wallet: Wallet;

  let amount: string = "";
  let loading: boolean = false;
@@ -34,14 +34,14 @@
    loading = true;
    try {
      const currentTime = new Date().getTime();
-
      const timelock = await calculateTimeLock(amount, $session.signer, config);
+
      const timelock = await calculateTimeLock(amount, $session.signer, wallet);
      const lastWithdrawal = await lastWithdrawalByUser(
        $session.signer,
-
        config,
+
        wallet,
      );
      const maxWithdrawAmount = await getMaxWithdrawAmount(
        $session.signer,
-
        config,
+
        wallet,
      );

      if (toWei(amount).gt(maxWithdrawAmount)) {
@@ -120,14 +120,14 @@
<main>
  <div class="title">
    Obtain RAD tokens on <span class="txt-bold">
-
      {capitalize(config.network.name)}
+
      {capitalize(wallet.network.name)}
    </span>
  </div>

-
  {#if config.network.name === "homestead"}
+
  {#if wallet.network.name === "homestead"}
    <div class="subtitle">
      To get RAD tokens on <span class="txt-bold">
-
        {capitalize(config.network.name)},
+
        {capitalize(wallet.network.name)},
      </span>
      please
      <br />
@@ -142,7 +142,7 @@
  {:else if !$session}
    <div class="subtitle">
      To get RAD tokens on <span class="txt-bold">
-
        {capitalize(config.network.name)}
+
        {capitalize(wallet.network.name)}
      </span>
      &#8203;,
      <br />
modified src/base/faucet/Routes.svelte
@@ -1,16 +1,16 @@
<script lang="ts">
  import { Route } from "svelte-routing";
  import Index from "@app/base/faucet/Index.svelte";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import Withdraw from "./Withdraw.svelte";

-
  export let config: Config;
+
  export let wallet: Wallet;
</script>

<Route path="faucet">
-
  <Index {config} />
+
  <Index {wallet} />
</Route>

<Route path="faucet/withdraw">
-
  <Withdraw {config} />
+
  <Withdraw {wallet} />
</Route>
modified src/base/faucet/Withdraw.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
  import { onMount } from "svelte";
  import { navigate } from "svelte-routing";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import Loading from "@app/Loading.svelte";
  import Modal from "@app/Modal.svelte";
  import ErrorModal from "@app/ErrorModal.svelte";
@@ -11,7 +11,7 @@
  import { session } from "@app/session";
  import Button from "@app/Button.svelte";

-
  export let config: Config;
+
  export let wallet: Wallet;

  let error: Error;
  const amount: string = window.history.state.amount;
@@ -27,7 +27,7 @@
    try {
      if ($session) {
        state.status = Status.Signing;
-
        const tx = await withdraw(amount, $session.signer, config);
+
        const tx = await withdraw(amount, $session.signer, wallet);
        state.status = Status.Pending;
        await tx.wait();
        state.status = Status.Success;
modified src/base/faucet/lib.ts
@@ -1,39 +1,42 @@
import * as ethers from "ethers";

-
import type { Config } from "@app/config";
+
import type { Wallet } from "@app/wallet";
import { assert } from "@app/error";
import type { TransactionResponse } from "@ethersproject/providers";
import { toWei } from "@app/utils";
import type { WalletConnectSigner } from "@app/WalletConnectSigner";
import type { TypedDataSigner } from "@ethersproject/abstract-signer";
+
import ethereumContractAbis from "@app/ethereum/contractAbis.json";

type Signer = (ethers.Signer & TypedDataSigner) | WalletConnectSigner | null;

export async function withdraw(
  amount: string,
  signer: Signer,
-
  config: Config,
+
  wallet: Wallet,
): Promise<TransactionResponse> {
  assert(signer);
+
  assert(wallet.radToken.faucet);

  const faucet = new ethers.Contract(
-
    config.radToken.faucet,
-
    config.abi.faucet,
+
    wallet.radToken.faucet,
+
    ethereumContractAbis.faucet,
    signer,
  );

-
  return faucet.withdraw(config.radToken.address, toWei(amount));
+
  return faucet.withdraw(wallet.radToken.address, toWei(amount));
}

export async function getMaxWithdrawAmount(
  signer: Signer,
-
  config: Config,
+
  wallet: Wallet,
): Promise<ethers.BigNumber> {
  assert(signer);
+
  assert(wallet.radToken.faucet);

  const faucet = new ethers.Contract(
-
    config.radToken.faucet,
-
    config.abi.faucet,
+
    wallet.radToken.faucet,
+
    ethereumContractAbis.faucet,
    signer,
  );

@@ -43,13 +46,14 @@ export async function getMaxWithdrawAmount(
export async function calculateTimeLock(
  amount: string,
  signer: Signer,
-
  config: Config,
+
  wallet: Wallet,
): Promise<ethers.BigNumber> {
  assert(signer);
+
  assert(wallet.radToken.faucet);

  const faucet = new ethers.Contract(
-
    config.radToken.faucet,
-
    config.abi.faucet,
+
    wallet.radToken.faucet,
+
    ethereumContractAbis.faucet,
    signer,
  );

@@ -58,15 +62,16 @@ export async function calculateTimeLock(

export async function lastWithdrawalByUser(
  signer: Signer,
-
  config: Config,
+
  wallet: Wallet,
): Promise<ethers.BigNumber> {
  assert(signer);
+
  assert(wallet.radToken.faucet);

  const address = signer.getAddress();

  const faucet = new ethers.Contract(
-
    config.radToken.faucet,
-
    config.abi.faucet,
+
    wallet.radToken.faucet,
+
    ethereumContractAbis.faucet,
    signer,
  );

modified src/base/home/Index.svelte
@@ -1,6 +1,5 @@
<script lang="ts">
  import { navigate } from "svelte-routing";
-
  import type { Config } from "@app/config";
  import Loading from "@app/Loading.svelte";
  import Widget from "@app/base/projects/Widget.svelte";
  import type { ProjectInfo } from "@app/project";
@@ -9,8 +8,7 @@
  import * as proj from "@app/project";
  import Message from "@app/Message.svelte";
  import { setOpenGraphMetaTag } from "@app/utils";
-

-
  export let config: Config;
+
  import config from "@app/config.json";

  setOpenGraphMetaTag([
    { prop: "og:title", content: "Radicle Interface" },
modified src/base/orgs/Org.ts
@@ -1,10 +1,11 @@
-
import type { Config } from "@app/config";
+
import type { Wallet } from "@app/wallet";

import * as ethers from "ethers";

import * as cache from "@app/cache";
import * as utils from "@app/utils";
import { assert } from "@app/error";
+
import ethereumContractAbis from "@app/ethereum/contractAbis.json";

export class Org {
  address: string;
@@ -19,8 +20,8 @@ export class Org {
    this.name = name;
  }

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

    try {
      const [owner, resolved] = await resolveOrgOwner(org);
@@ -40,8 +41,12 @@ export class Org {
}

export const getOrgContract = cache.cached(
-
  async (addressOrName: string, config: Config) => {
-
    return new ethers.Contract(addressOrName, config.abi.org, config.provider);
+
  async (addressOrName: string, wallet: Wallet) => {
+
    return new ethers.Contract(
+
      addressOrName,
+
      ethereumContractAbis.org,
+
      wallet.provider,
+
    );
  },
  addressOrName => addressOrName,
);
modified src/base/projects/Issue.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { Blob, Project } from "@app/project";
  import { canonicalize, capitalize } from "@app/utils";
  import { formatObjectId } from "@app/cobs";
@@ -9,7 +9,7 @@

  export let issue: Issue;
  export let project: Project;
-
  export let config: Config;
+
  export let wallet: Wallet;

  // Get an image blob based on a relative path.
  const getImage = async (imagePath: string): Promise<Blob> => {
@@ -129,20 +129,20 @@
      </div>
    </div>
    <Authorship
-
      {config}
+
      {wallet}
      author={issue.author}
      timestamp={issue.timestamp}
      caption="opened on" />
  </header>
  <main>
    <div class="comments">
-
      <Comment comment={issue.comment} {getImage} {config} />
+
      <Comment comment={issue.comment} {getImage} {wallet} />
      {#each issue.discussion as comment}
-
        <Comment {comment} {getImage} {config} />
+
        <Comment {comment} {getImage} {wallet} />
        {#if comment.replies}
          <div class="replies">
            {#each comment.replies as reply}
-
              <Comment comment={reply} {getImage} {config} />
+
              <Comment comment={reply} {getImage} {wallet} />
            {/each}
          </div>
        {/if}
modified src/base/projects/Issue/IssueTeaser.svelte
@@ -2,13 +2,13 @@
  import { onMount } from "svelte";
  import { formatObjectId } from "@app/cobs";
  import type { Issue } from "@app/issue";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import { Profile, ProfileType } from "@app/profile";

  import Authorship from "@app/Authorship.svelte";

  export let issue: Issue;
-
  export let config: Config;
+
  export let wallet: Wallet;

  let profile: Profile | null = null;

@@ -17,7 +17,7 @@
      profile = await Profile.get(
        issue.author.profile.ens.name,
        ProfileType.Minimal,
-
        config,
+
        wallet,
      );
    }
  });
@@ -114,7 +114,7 @@
    </div>
    <Authorship
      {profile}
-
      {config}
+
      {wallet}
      caption="opened"
      author={issue.author}
      timestamp={issue.timestamp} />
modified src/base/projects/Issues.svelte
@@ -3,7 +3,7 @@
</script>

<script lang="ts">
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { Issue } from "@app/issue";
  import type { ToggleButtonOption } from "@app/ToggleButton.svelte";

@@ -16,7 +16,7 @@
  import Placeholder from "@app/Placeholder.svelte";
  import ToggleButton from "@app/ToggleButton.svelte";

-
  export let config: Config;
+
  export let wallet: Wallet;
  export let issues: Issue[];
  export let project: Project;
  export let state: State;
@@ -86,7 +86,7 @@
              path: null,
            });
          }}>
-
          <IssueTeaser {config} {issue} />
+
          <IssueTeaser {wallet} {issue} />
        </div>
      {/each}
    </div>
modified src/base/projects/Patch.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import { Project, ProjectContent } from "@app/project";
  import { capitalize } from "@app/utils";
  import { Patch, PatchTab } from "@app/patch";
@@ -14,7 +14,7 @@

  export let patch: Patch;
  export let project: Project;
-
  export let config: Config;
+
  export let wallet: Wallet;

  const onSwitch = ({ detail }: { detail: PatchTab }) => {
    activeTab = detail;
@@ -116,7 +116,7 @@
    </div>
    <Authorship
      noAvatar
-
      {config}
+
      {wallet}
      author={patch.author}
      timestamp={patch.timestamp}
      caption="opened" />
@@ -132,7 +132,7 @@
  <main>
    {#if activeTab === PatchTab.Timeline}
      <div class="flex">
-
        <PatchTimeline {patch} {revisionNumber} {config} {project} />
+
        <PatchTimeline {patch} {revisionNumber} {wallet} {project} />
        <PatchSideBar {patch} />
      </div>
    {:else if activeTab === PatchTab.Diff && revision.changeset}
modified src/base/projects/Patch/PatchTeaser.svelte
@@ -2,13 +2,13 @@
  import { onMount } from "svelte";
  import { formatObjectId } from "@app/cobs";
  import type { Patch } from "@app/patch";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import { Profile, ProfileType } from "@app/profile";

  import Authorship from "@app/Authorship.svelte";

  export let patch: Patch;
-
  export let config: Config;
+
  export let wallet: Wallet;

  let profile: Profile | null = null;

@@ -17,7 +17,7 @@
      profile = await Profile.get(
        patch.author.profile.ens.name,
        ProfileType.Minimal,
-
        config,
+
        wallet,
      );
    }
  });
@@ -114,7 +114,7 @@
    </div>
    <Authorship
      {profile}
-
      {config}
+
      {wallet}
      caption="opened"
      author={patch.author}
      timestamp={patch.timestamp} />
modified src/base/projects/Patch/PatchTimeline.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import { type Patch, TimelineType } from "@app/patch";
  import { formatSeedId } from "@app/utils";
  import { canonicalize } from "@app/utils";
@@ -10,7 +10,7 @@

  export let patch: Patch;
  export let revisionNumber: number;
-
  export let config: Config;
+
  export let wallet: Wallet;
  export let project: Project;

  $: timeline = patch.createTimeline(revisionNumber);
@@ -49,26 +49,26 @@
          }}
          caption={`merged to ${formatSeedId(element.inner.peer.id)}`}
          timestamp={element.timestamp}
-
          {config} />
+
          {wallet} />
      </div>
    {:else if element.type === TimelineType.Review && element.inner.author.profile?.ens?.name}
      <div class="margin-left">
-
        <Review review={element.inner} {config} {getImage} />
+
        <Review review={element.inner} {wallet} {getImage} />
      </div>
    {:else if element.type === TimelineType.Comment}
      <div class="margin-left">
        <!-- Since the element variable only experiences changes on the inner property,
        this component has to be forced to be rerendered when element.inner changes -->
        {#key element.inner}
-
          <Comment comment={element.inner} {config} {getImage} />
+
          <Comment comment={element.inner} {wallet} {getImage} />
        {/key}
      </div>
    {:else if element.type === TimelineType.Thread}
      <div class="margin-left">
-
        <Comment comment={element.inner} {config} {getImage} />
+
        <Comment comment={element.inner} {wallet} {getImage} />
        <div class="replies">
          {#each element.inner.replies as comment}
-
            <Comment caption="replied" {comment} {config} {getImage} />
+
            <Comment caption="replied" {comment} {wallet} {getImage} />
          {/each}
        </div>
      </div>
modified src/base/projects/Patches.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
  type State = "proposed" | "draft" | "archived";

-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { Patch } from "@app/patch";
  import type { ToggleButtonOption } from "@app/ToggleButton.svelte";

@@ -14,7 +14,7 @@
  import { groupPatches } from "@app/patch";

  export let state: State = "proposed";
-
  export let config: Config;
+
  export let wallet: Wallet;
  export let patches: Patch[];
  export let project: Project;

@@ -83,7 +83,7 @@
              path: null,
            });
          }}>
-
          <PatchTeaser {config} {patch} />
+
          <PatchTeaser {wallet} {patch} />
        </div>
      {/each}
    </div>
modified src/base/projects/Project.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { State as IssueState } from "./Issues.svelte";

  import * as proj from "@app/project";
@@ -23,13 +23,13 @@
  import Patch from "./Patch.svelte";

  export let peer: string | null = null;
-
  export let config: Config;
+
  export let wallet: Wallet;
  export let project: proj.Project;
  export let content: proj.ProjectContent;
  export let revision: string | null;

  const parentName = project.profile
-
    ? formatProfile(project.profile.nameOrAddress, config)
+
    ? formatProfile(project.profile.nameOrAddress, wallet)
    : null;
  let pageTitle = parentName ? `${parentName}/${project.name}` : project.name;

@@ -120,7 +120,7 @@
    <Async
      fetch={issue.Issue.getIssues(project.urn, project.seed.api)}
      let:result>
-
      <Issues {project} state={issueFilter} {config} issues={result} />
+
      <Issues {project} state={issueFilter} {wallet} issues={result} />
    </Async>
  {:else if content === proj.ProjectContent.Issue && $browserStore.issue}
    <Async
@@ -130,13 +130,13 @@
        project.seed.api,
      )}
      let:result>
-
      <Issue {project} {config} issue={result} />
+
      <Issue {project} {wallet} issue={result} />
    </Async>
  {:else if content === proj.ProjectContent.Patches}
    <Async
      fetch={patch.Patch.getPatches(project.urn, project.seed.api)}
      let:result>
-
      <Patches {project} {config} patches={result} />
+
      <Patches {project} {wallet} patches={result} />
    </Async>
  {:else if content === proj.ProjectContent.Patch && $browserStore.patch}
    <Async
@@ -146,7 +146,7 @@
        project.seed.api,
      )}
      let:result>
-
      <Patch {project} {config} patch={result} />
+
      <Patch {project} {wallet} patch={result} />
    </Async>
  {/if}
{:else}
modified src/base/projects/ProjectRoute.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import type { Writable } from "svelte/store";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import { formatLocationHash } from "@app/utils";
  import * as proj from "@app/project";
  import type { RouteLocation } from "@app/index";
@@ -15,7 +15,7 @@
  export let peer: string | null;
  export let content: proj.ProjectContent = proj.ProjectContent.Tree;
  export let project: proj.Project;
-
  export let config: Config;
+
  export let wallet: Wallet;
  export let location: RouteLocation | null = null;

  const browse: proj.BrowseTo = { content, peer, path: "/" };
@@ -56,4 +56,4 @@
  revision={browser.revision || head}
  content={browser.content}
  {project}
-
  {config} />
+
  {wallet} />
modified src/base/projects/Routes.svelte
@@ -1,20 +1,20 @@
<script lang="ts">
  import { Route } from "svelte-routing";
  import View from "@app/base/projects/View.svelte";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import Redirect from "@app/Redirect.svelte";

-
  export let config: Config;
+
  export let wallet: Wallet;
</script>

<!-- With a seed context -->

<Route path="/seeds/:seed/:id/*" let:params>
-
  <View {config} seedHost={params.seed} id={params.id} />
+
  <View {wallet} seedHost={params.seed} id={params.id} />
</Route>

<Route path="/seeds/:seed/:id/remotes/:peer/*" let:params>
-
  <View {config} seedHost={params.seed} peer={params.peer} id={params.id} />
+
  <View {wallet} seedHost={params.seed} peer={params.peer} id={params.id} />
</Route>

<!-- Explicit user and org context, will at some point be replaced by the generic route -->
@@ -29,12 +29,12 @@

<Route path="/:profile/:id/remotes/:peer/*" let:params>
  <View
-
    {config}
+
    {wallet}
    profileName={params.profile}
    id={params.id}
    peer={params.peer} />
</Route>

<Route path="/:profile/:id/*" let:params>
-
  <View {config} profileName={params.profile} id={params.id} />
+
  <View {wallet} profileName={params.profile} id={params.id} />
</Route>
modified src/base/projects/View.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import { Route, Router } from "svelte-routing";
  import { Project, ProjectContent } from "@app/project";
  import Loading from "@app/Loading.svelte";
@@ -11,7 +11,7 @@
  export let seedHost: string | null = null;
  export let profileName: string | null = null; // Address or name of parent profile.
  export let peer: string | null = null;
-
  export let config: Config;
+
  export let wallet: Wallet;
</script>

<style>
@@ -37,7 +37,7 @@
</style>

<main>
-
  {#await Project.get(id, peer, profileName, seedHost, config)}
+
  {#await Project.get(id, peer, profileName, seedHost, wallet)}
    <header>
      <Loading center />
    </header>
@@ -45,10 +45,10 @@
    <Router>
      <!-- The default action is to render Browser with the default branch head -->
      <Route path="/">
-
        <ProjectRoute content={ProjectContent.Tree} {peer} {project} {config} />
+
        <ProjectRoute content={ProjectContent.Tree} {peer} {project} {wallet} />
      </Route>
      <Route path="/tree">
-
        <ProjectRoute content={ProjectContent.Tree} {peer} {project} {config} />
+
        <ProjectRoute content={ProjectContent.Tree} {peer} {project} {wallet} />
      </Route>
      <Route path="/tree/*" let:params let:location>
        <ProjectRoute
@@ -57,7 +57,7 @@
          {location}
          {peer}
          {project}
-
          {config} />
+
          {wallet} />
      </Route>

      <Route path="/history">
@@ -65,7 +65,7 @@
          content={ProjectContent.History}
          {peer}
          {project}
-
          {config} />
+
          {wallet} />
      </Route>
      <Route path="/history/*" let:params let:location>
        <ProjectRoute
@@ -74,7 +74,7 @@
          {location}
          {peer}
          {project}
-
          {config} />
+
          {wallet} />
      </Route>

      <Route path="/commits/:commit" let:params>
@@ -83,7 +83,7 @@
          content={ProjectContent.Commit}
          {peer}
          {project}
-
          {config} />
+
          {wallet} />
      </Route>
      <Route path="/commits/*" let:params let:location>
        <ProjectRoute
@@ -92,7 +92,7 @@
          {location}
          {peer}
          {project}
-
          {config} />
+
          {wallet} />
      </Route>

      <Route path="/issues" let:location>
@@ -101,7 +101,7 @@
          {peer}
          {project}
          {location}
-
          {config} />
+
          {wallet} />
      </Route>
      <Route path="/issues/:issue" let:params let:location>
        <ProjectRoute
@@ -110,7 +110,7 @@
          {peer}
          {project}
          {location}
-
          {config} />
+
          {wallet} />
      </Route>

      <Route path="/patches">
@@ -118,7 +118,7 @@
          content={ProjectContent.Patches}
          {peer}
          {project}
-
          {config} />
+
          {wallet} />
      </Route>
      <Route path="/patches/:patch" let:params>
        <ProjectRoute
@@ -126,7 +126,7 @@
          patch={params.patch}
          {peer}
          {project}
-
          {config} />
+
          {wallet} />
      </Route>
    </Router>
  {:catch}
modified src/base/registrations/Index.svelte
@@ -1,11 +1,11 @@
<script lang="ts">
  import { navigate } from "svelte-routing";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";

  import TextInput from "@app/TextInput.svelte";
  import Button from "@app/Button.svelte";

-
  export let config: Config;
+
  export let wallet: Wallet;

  let input = "";
  let valid: boolean = false;
@@ -79,15 +79,15 @@

<main>
  <div class="title">
-
    Register a <span class="txt-bold">{config.registrar.domain}</span>
+
    Register a <span class="txt-bold">{wallet.registrar.domain}</span>
    name
  </div>

  <div class="subtitle">
    Register a unique name with our ENS registrar, under <br />
    the
-
    <span class="txt-bold">{config.registrar.domain}</span>
-
    domain (e.g. cloudhead.{config.registrar.domain}).
+
    <span class="txt-bold">{wallet.registrar.domain}</span>
+
    domain (e.g. cloudhead.{wallet.registrar.domain}).
    <br />
    Radicle names never expire and free to register.
  </div>
@@ -100,7 +100,7 @@
      {valid}
      {validationMessage}>
      <svelte:fragment slot="right">
-
        .{config.registrar.domain}
+
        .{wallet.registrar.domain}
      </svelte:fragment>
    </TextInput>

modified src/base/registrations/New.svelte
@@ -3,7 +3,7 @@
  import { navigate } from "svelte-routing";
  import { formatAddress } from "@app/utils";
  import { session } from "@app/session";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";

  import Connect from "@app/Connect.svelte";
  import Modal from "@app/Modal.svelte";
@@ -20,7 +20,7 @@
    NameUnavailable,
  }

-
  export let config: Config;
+
  export let wallet: Wallet;
  export let name: string;
  export let owner: string | null;

@@ -43,7 +43,7 @@

  onMount(async () => {
    try {
-
      const isAvailable = await registrar(config).available(name);
+
      const isAvailable = await registrar(wallet).available(name);

      if (isAvailable) {
        state = State.NameAvailable;
@@ -76,7 +76,7 @@
  </span>

  <span slot="subtitle">
-
    {name}.{config.registrar.domain}
+
    {name}.{wallet.registrar.domain}
  </span>

  <span slot="body">
@@ -113,7 +113,7 @@
        <Connect
          caption="Connect to register"
          buttonVariant="primary"
-
          {config} />
+
          {wallet} />
      {/if}

      <Button on:click={() => navigate("/registrations")} variant="text">
modified src/base/registrations/Routes.svelte
@@ -5,26 +5,26 @@
  import Submit from "@app/base/registrations/Submit.svelte";
  import View from "@app/base/registrations/View.svelte";
  import ErrorModal from "@app/ErrorModal.svelte";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { Session } from "@app/session";
  import { getSearchParam } from "@app/utils";

  export let session: Session | null;
-
  export let config: Config;
+
  export let wallet: Wallet;
</script>

<Route path="registrations">
-
  <Index {config} />
+
  <Index {wallet} />
</Route>

<Route path="registrations/:name/form" let:params let:location>
-
  <New {config} name={params.name} owner={getSearchParam("owner", location)} />
+
  <New {wallet} name={params.name} owner={getSearchParam("owner", location)} />
</Route>

<Route path="registrations/:name/submit" let:params let:location>
  {#if session}
    <Submit
-
      {config}
+
      {wallet}
      name={params.name}
      owner={getSearchParam("owner", location)}
      {session} />
@@ -36,5 +36,5 @@
</Route>

<Route path="registrations/:domain" let:params>
-
  <View {config} domain={params.domain} />
+
  <View {wallet} domain={params.domain} />
</Route>
modified src/base/registrations/Submit.svelte
@@ -3,7 +3,7 @@
  import { onMount } from "svelte";
  import { navigate } from "svelte-routing";
  import type { Session } from "@app/session";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import Loading from "@app/Loading.svelte";
  import Modal from "@app/Modal.svelte";
  import ErrorModal from "@app/ErrorModal.svelte";
@@ -12,7 +12,7 @@

  import { registerName, State, state } from "./registrar";

-
  export let config: Config;
+
  export let wallet: Wallet;
  export let name: string;
  export let owner: string | null;
  export let session: Session;
@@ -21,13 +21,13 @@
  const registrationOwner = owner || session.address;

  const view = () =>
-
    navigate(`/registrations/${name}.${config.registrar.domain}`, {
+
    navigate(`/registrations/${name}.${wallet.registrar.domain}`, {
      state: { retry: true },
    });

  onMount(async () => {
    try {
-
      await registerName(name, registrationOwner, config);
+
      await registerName(name, registrationOwner, wallet);
    } catch (e: any) {
      console.error("Error", e);

@@ -37,7 +37,7 @@
  });

  let latestBlock: number;
-
  config.provider.on("block", (block: number) => {
+
  wallet.provider.on("block", (block: number) => {
    latestBlock = block;
  });
</script>
@@ -67,7 +67,7 @@
      {:else}
        <div>🌐</div>
      {/if}
-
      {name}.{config.registrar.domain}
+
      {name}.{wallet.registrar.domain}
    </span>

    <span slot="subtitle">
modified src/base/registrations/Update.svelte
@@ -3,7 +3,7 @@
  import { setRecords } from "./resolver";
  import type { EnsRecord } from "./resolver";
  import type { Registration } from "./registrar";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import Loading from "@app/Loading.svelte";
  import Modal from "@app/Modal.svelte";
  import type { State } from "@app/utils";
@@ -11,7 +11,7 @@
  import Button from "@app/Button.svelte";

  export let domain: string;
-
  export let config: Config;
+
  export let wallet: Wallet;
  export let records: EnsRecord[];
  export let registration: Registration;

@@ -29,7 +29,7 @@
        domain,
        records,
        registration.resolver,
-
        config,
+
        wallet,
      );
      state.status = Status.Pending;
      await tx.wait();
modified src/base/registrations/View.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
  import { onMount } from "svelte";
  import { link, navigate } from "svelte-routing";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { ethers } from "ethers";
  import { session } from "@app/session";
  import Loading from "@app/Loading.svelte";
@@ -12,6 +12,7 @@
  import ErrorModal from "@app/ErrorModal.svelte";
  import { isAddressEqual, isReverseRecordSet } from "@app/utils";
  import Button from "@app/Button.svelte";
+
  import { defaultHttpApiPort } from "@app/base/seeds/Seed";

  import { getRegistration, getOwner } from "./registrar";
  import type { EnsRecord } from "./resolver";
@@ -32,7 +33,7 @@
    | { status: Status.Failed; error: string };

  export let domain: string;
-
  export let config: Config;
+
  export let wallet: Wallet;

  domain = domain.toLowerCase();

@@ -52,10 +53,10 @@
        reverseRecord = await isReverseRecordSet(
          r.profile.address,
          domain,
-
          config,
+
          wallet,
        );
      }
-
      const owner = await getOwner(domain, config);
+
      const owner = await getOwner(domain, wallet);
      resolver = r.resolver;

      fields = [
@@ -134,7 +135,7 @@
          description:
            "The seed host address. " +
            "Only domain names with TLS are supported. " +
-
            `HTTP(S) API requests use port ${config.seed.api.port}.`,
+
            `HTTP(S) API requests use port ${defaultHttpApiPort}.`,
          value: r.profile.seed?.host ?? "",
          editable: true,
        },
@@ -158,7 +159,7 @@
  }

  onMount(() => {
-
    getRegistration(domain, config, resolver)
+
    getRegistration(domain, wallet, resolver)
      .then(parseRecords)
      .catch(err => {
        state = { status: Status.Failed, error: err };
@@ -182,7 +183,7 @@
    state.status === Status.NotFound &&
    retries > 0
  ) {
-
    getRegistration(domain, config, resolver)
+
    getRegistration(domain, wallet, resolver)
      .then(parseRecords)
      .catch(err => {
        state = { status: Status.Failed, error: err };
@@ -279,7 +280,7 @@
      </div>
    </header>
    <Form
-
      {config}
+
      {wallet}
      {editable}
      {fields}
      on:save={onSave}
@@ -288,7 +289,7 @@

  {#if updateRecords}
    <Update
-
      {config}
+
      {wallet}
      {domain}
      on:close={() => (updateRecords = null)}
      registration={state.registration}
modified src/base/registrations/registrar.ts
@@ -5,11 +5,12 @@ import type { EnsResolver } from "@ethersproject/providers";
import type { TypedDataSigner } from "@ethersproject/abstract-signer";
import * as session from "@app/session";
import { Failure } from "@app/error";
-
import type { Config } from "@app/config";
+
import type { Wallet } from "@app/wallet";
import { isFulfilled, unixTime } from "@app/utils";
import { assert } from "@app/error";
import { Seed, InvalidSeed } from "@app/base/seeds/Seed";
import * as cache from "@app/cache";
+
import ethereumContractAbis from "@app/ethereum/contractAbis.json";

export interface Registration {
  profile: EnsProfile;
@@ -65,13 +66,13 @@ state.subscribe((s: Connection) => {

export async function getRegistration(
  name: string,
-
  config: Config,
+
  wallet: Wallet,
  resolver?: EnsResolver | null,
): Promise<Registration | null> {
  name = name.toLowerCase();

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

    if (!resolver) {
      return null;
@@ -117,15 +118,12 @@ export async function getRegistration(
  // If no seed provided profile.seed ends up being undefined
  if (seedHost && seedId) {
    try {
-
      profile.seed = new Seed(
-
        {
-
          host: seedHost,
-
          id: seedId,
-
          git: seedGit,
-
          api: seedApi,
-
        },
-
        config,
-
      );
+
      profile.seed = new Seed({
+
        host: seedHost,
+
        id: seedId,
+
        git: seedGit,
+
        api: seedApi,
+
      });
    } catch (e: any) {
      console.debug(e, seedHost, seedId);
      profile.seed = new InvalidSeed(seedHost, seedId);
@@ -137,12 +135,12 @@ export async function getRegistration(

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

-
  resolver = resolver ?? (await getResolver(name, config));
+
  resolver = resolver ?? (await getResolver(name, wallet));
  if (!resolver) {
    return null;
  }
@@ -151,12 +149,12 @@ export async function getAvatar(

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

-
  resolver = resolver ?? (await getResolver(name, config));
+
  resolver = resolver ?? (await getResolver(name, wallet));
  if (!resolver) {
    return null;
  }
@@ -174,31 +172,31 @@ export async function getSeed(
  }

  try {
-
    return new Seed({ host, id, git, api }, config);
+
    return new Seed({ host, id, git, api });
  } catch (e: any) {
    console.debug(e, host, id);
    return new InvalidSeed(id, host);
  }
}

-
export function registrar(config: Config): ethers.Contract {
+
export function registrar(wallet: Wallet): ethers.Contract {
  return new ethers.Contract(
-
    config.registrar.address,
-
    config.abi.registrar,
-
    config.provider,
+
    wallet.registrar.address,
+
    ethereumContractAbis.registrar,
+
    wallet.provider,
  );
}

-
export async function registrationFee(config: Config): Promise<BigNumber> {
-
  return await registrar(config).registrationFeeRad();
+
export async function registrationFee(wallet: Wallet): Promise<BigNumber> {
+
  return await registrar(wallet).registrationFeeRad();
}

export async function registerName(
  name: string,
  owner: string,
-
  config: Config,
+
  wallet: Wallet,
): Promise<void> {
-
  assert(config.signer, "signer is not available");
+
  assert(wallet.signer, "signer is not available");

  if (!name) return;

@@ -210,9 +208,9 @@ export async function registerName(
  try {
    // Try to recover an existing commitment.
    if (commitment && commitment.name === name && commitment.owner === owner) {
-
      await register(name, owner, commitment.salt, config);
+
      await register(name, owner, commitment.salt, wallet);
    } else {
-
      await commitAndRegister(name, owner, config);
+
      await commitAndRegister(name, owner, wallet);
    }
  } catch (e: any) {
    throw {
@@ -226,14 +224,14 @@ export async function registerName(
async function commitAndRegister(
  name: string,
  owner: string,
-
  config: Config,
+
  wallet: Wallet,
): Promise<void> {
  const salt = ethers.utils.randomBytes(32);
-
  const minAge = (await registrar(config).minCommitmentAge()).toNumber();
-
  const fee = await registrationFee(config);
+
  const minAge = (await registrar(wallet).minCommitmentAge()).toNumber();
+
  const fee = await registrationFee(wallet);
  // Avoids gas spent by the owner, trying to commit to a name and not having
  // enough RAD balance
-
  if ((await config.token.balanceOf(owner)).lt(fee)) {
+
  if ((await wallet.token.balanceOf(owner)).lt(fee)) {
    throw {
      type: Failure.InsufficientBalance,
      message: "Not enough RAD funds",
@@ -241,8 +239,8 @@ async function commitAndRegister(
  }
  name = name.toLowerCase();

-
  await commit(name, owner, salt, fee, minAge, config);
-
  await register(name, owner, salt, config);
+
  await commit(name, owner, salt, fee, minAge, wallet);
+
  await register(name, owner, salt, wallet);
}

async function commit(
@@ -251,26 +249,26 @@ async function commit(
  salt: Uint8Array,
  fee: BigNumber,
  minAge: number,
-
  config: Config,
+
  wallet: Wallet,
): Promise<void> {
-
  assert(config.signer, "signer is not available");
+
  assert(wallet.signer, "signer is not available");

  const commitment = makeCommitment(name, owner, salt);
-
  const spender = config.registrar.address;
+
  const spender = wallet.registrar.address;
  const deadline = ethers.BigNumber.from(unixTime()).add(3600); // Expire one hour from now.
-
  const token = config.token;
+
  const token = wallet.token;

  let tx = null;

  if (fee.isZero()) {
    state.set({ connection: State.SigningCommit });

-
    tx = await registrar(config)
-
      .connect(config.signer)
+
    tx = await registrar(wallet)
+
      .connect(wallet.signer)
      .commit(commitment, { gasLimit: 180000 });
  } else {
    const signature = await permitSignature(
-
      config.signer,
+
      wallet.signer,
      token,
      spender,
      fee,
@@ -279,8 +277,8 @@ async function commit(

    state.set({ connection: State.SigningCommit });

-
    tx = await registrar(config)
-
      .connect(config.signer)
+
    tx = await registrar(wallet)
+
      .connect(wallet.signer)
      .commitWithPermit(
        commitment,
        owner,
@@ -361,13 +359,13 @@ async function register(
  name: string,
  owner: string,
  salt: Uint8Array,
-
  config: Config,
+
  wallet: Wallet,
) {
-
  assert(config.signer, "signer is not available");
+
  assert(wallet.signer, "signer is not available");
  state.set({ connection: State.SigningRegister });

-
  const tx = await registrar(config)
-
    .connect(config.signer)
+
  const tx = await registrar(wallet)
+
    .connect(wallet.signer)
    .register(name, owner, ethers.BigNumber.from(salt), { gasLimit: 150000 });
  state.set({ connection: State.Registering });

@@ -387,16 +385,16 @@ function makeCommitment(name: string, owner: string, salt: Uint8Array): string {
  return ethers.utils.keccak256(bytes);
}

-
export async function getOwner(name: string, config: Config): Promise<string> {
-
  const ensAddr = config.provider.network.ensAddress;
+
export async function getOwner(name: string, wallet: Wallet): Promise<string> {
+
  const ensAddr = wallet.provider.network.ensAddress;
  if (!ensAddr) {
    throw new Error("ENS address is not defined");
  }

  const registry = new ethers.Contract(
    ensAddr,
-
    config.abi.ens,
-
    config.provider,
+
    ethereumContractAbis.ens,
+
    wallet.provider,
  );
  const owner = await registry.owner(ethers.utils.namehash(name));

@@ -404,8 +402,8 @@ export async function getOwner(name: string, config: Config): Promise<string> {
}

export const getResolver = cache.cached(
-
  async (name: string, config: Config) => {
-
    return await config.provider.getResolver(name);
+
  async (name: string, wallet: Wallet) => {
+
    return await wallet.provider.getResolver(name);
  },
  name => name,
  { max: 1000 },
modified src/base/registrations/resolver.ts
@@ -1,8 +1,9 @@
import type { TransactionResponse } from "@ethersproject/providers";
import type { EnsResolver } from "@ethersproject/providers";
import { ethers } from "ethers";
-
import type { Config } from "@app/config";
+
import type { Wallet } from "@app/wallet";
import { assert } from "@app/error";
+
import ethereumContractAbis from "@app/ethereum/contractAbis.json";

export type EnsRecord = { name: string; value: string };

@@ -10,19 +11,19 @@ export async function setRecords(
  name: string,
  records: EnsRecord[],
  resolver: EnsResolver,
-
  config: Config,
+
  wallet: Wallet,
): Promise<TransactionResponse> {
-
  assert(config.signer, "no signer available");
+
  assert(wallet.signer, "no signer available");

  const resolverContract = new ethers.Contract(
    resolver.address,
-
    config.abi.resolver,
-
    config.signer,
+
    ethereumContractAbis.resolver,
+
    wallet.signer,
  );
  const node = ethers.utils.namehash(name);

  const calls = [];
-
  const iface = new ethers.utils.Interface(config.abi.resolver);
+
  const iface = new ethers.utils.Interface(ethereumContractAbis.resolver);

  for (const r of records) {
    switch (r.name) {
modified src/base/seeds/Routes.svelte
@@ -1,17 +1,17 @@
<script lang="ts">
  import { Route } from "svelte-routing";
  import View from "@app/base/seeds/View.svelte";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { Session } from "@app/session";

-
  export let config: Config;
+
  export let wallet: Wallet;
  export let session: Session | null;
</script>

<Route path="/seeds/radicle.local">
-
  <View {config} {session} host={"0.0.0.0"} />
+
  <View {wallet} {session} host={"0.0.0.0"} />
</Route>

<Route path="/seeds/:seed" let:params>
-
  <View {config} {session} host={params.seed} />
+
  <View {wallet} {session} host={params.seed} />
</Route>
modified src/base/seeds/Seed.ts
@@ -1,8 +1,8 @@
import { Request, type Host } from "@app/api";
-
import type { Config } from "@app/config";
import * as proj from "@app/project";
-
import { isDomain, isLocal } from "@app/utils";
+
import { isDomain } from "@app/utils";
import { assert } from "@app/error";
+
import { getSeedEmoji } from "@app/utils";

export interface Stats {
  projects: { count: number };
@@ -21,6 +21,10 @@ export class InvalidSeed {
  }
}

+
export const defaultHttpApiPort = 8777;
+
export const defaultLinkPort = 8776;
+
export const defaultGitPort = 443;
+

export class Seed {
  valid = true as const;

@@ -31,23 +35,20 @@ export class Seed {
  version?: string;
  emoji: string;

-
  constructor(
-
    seed: {
-
      host: string;
-
      id: string;
-
      git?: string | null;
-
      api?: string | null;
-
      version?: string | null;
-
    },
-
    cfg: Config,
-
  ) {
+
  constructor(seed: {
+
    host: string;
+
    id: string;
+
    git?: string | null;
+
    api?: string | null;
+
    version?: string | null;
+
  }) {
    assert(isDomain(seed.host), `invalid seed host: ${seed.host}`);
    assert(/^[a-z0-9]+$/.test(seed.id), `invalid seed id ${seed.id}`);

    let api = null;
    let git = null;
-
    let apiPort: number | null = cfg.seed.api.port;
-
    let gitPort: number | null = cfg.seed.git.port;
+
    let apiPort: number | null = defaultHttpApiPort;
+
    let gitPort: number | null = defaultGitPort;

    if (seed.api) {
      try {
@@ -71,14 +72,7 @@ export class Seed {
      assert(isDomain(git), `invalid seed git host ${git}`);
    }

-
    const meta = cfg.seeds.pinned[seed.host];
-
    if (meta) {
-
      this.emoji = meta.emoji;
-
    } else if (isLocal(seed.host)) {
-
      this.emoji = "🏠";
-
    } else {
-
      this.emoji = "🌱";
-
    }
+
    this.emoji = getSeedEmoji(seed.host);

    // The `git` and `api` keys being more specific take
    // precedence over the `host`, if available.
@@ -87,7 +81,7 @@ export class Seed {

    this.api = { host: api, port: apiPort };
    this.git = { host: git, port: gitPort };
-
    this.link = { host: seed.host, id: seed.id, port: cfg.seed.link.port };
+
    this.link = { host: seed.host, id: seed.id, port: defaultLinkPort };

    if (seed.version) {
      this.version = seed.version;
@@ -136,24 +130,21 @@ export class Seed {
    return new Request("/", host).get();
  }

-
  static async lookup(hostname: string, cfg: Config): Promise<Seed> {
-
    const host = { host: hostname, port: cfg.seed.api.port };
+
  static async lookup(hostname: string): Promise<Seed> {
+
    const host = { host: hostname, port: defaultHttpApiPort };
    const [info, peer] = await Promise.all([
      Seed.getInfo(host),
      Seed.getPeer(host),
    ]);

-
    return new Seed(
-
      {
-
        host: hostname,
-
        id: peer.id,
-
        version: info.version,
-
      },
-
      cfg,
-
    );
+
    return new Seed({
+
      host: hostname,
+
      id: peer.id,
+
      version: info.version,
+
    });
  }

-
  static async lookupMulti(hostnames: string[], cfg: Config): Promise<Seed[]> {
-
    return await Promise.all(hostnames.map(h => Seed.lookup(h, cfg)));
+
  static async lookupMulti(hostnames: string[]): Promise<Seed[]> {
+
    return await Promise.all(hostnames.map(h => Seed.lookup(h)));
  }
}
modified src/base/seeds/View.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { Stats } from "@app/base/seeds/Seed";
  import type { ProjectInfo } from "@app/project";
  import { formatSeedId, formatSeedHost } from "@app/utils";
@@ -17,7 +17,7 @@
  import { Project } from "@app/project";
  import type { Host } from "@app/api";

-
  export let config: Config;
+
  export let wallet: Wallet;
  export let session: Session | null;
  export let host: string;

@@ -108,7 +108,7 @@
  <title>{hostName}</title>
</svelte:head>

-
{#await Seed.lookup(host, config)}
+
{#await Seed.lookup(host)}
  <main class="layout-centered">
    <Loading center />
  </main>
@@ -129,19 +129,19 @@
              <span class="signed-in txt-small">Signed in as</span>
              <Address
                address={siweSession.address}
-
                {config}
+
                {wallet}
                small
                compact
                resolve />
            </div>
          {:else}
-
            <SiweConnect {seed} address={session.address} {config} />
+
            <SiweConnect {seed} address={session.address} {wallet} />
          {/if}
        {:else}
          <SiweConnect
            disabled
            {seed}
-
            {config}
+
            {wallet}
            tooltip={"Connect your wallet to sign in"} />
        {/if}
      </div>
modified src/base/users/User.ts
@@ -1,7 +1,8 @@
import * as ethers from "ethers";
-
import type { Config } from "@app/config";
+
import type { Wallet } from "@app/wallet";
import { assert } from "@app/error";
import type { TransactionResponse } from "@ethersproject/providers";
+
import ethereumContractAbis from "@app/ethereum/contractAbis.json";

export class User {
  address: string;
@@ -12,13 +13,13 @@ export class User {
    this.address = address.toLowerCase(); // Don't store address checksum.
  }

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

    const reverseRegistrar = new ethers.Contract(
-
      config.reverseRegistrar.address,
-
      config.abi.reverseRegistrar,
-
      config.signer,
+
      wallet.reverseRegistrar.address,
+
      ethereumContractAbis.reverseRegistrar,
+
      wallet.signer,
    );
    return reverseRegistrar.setName(name);
  }
modified src/base/vesting/Index.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import type { Session } from "@app/session";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import type { VestingInfo } from "./vesting";

  import * as utils from "@app/utils";
@@ -10,7 +10,7 @@
  import TextInput from "@app/TextInput.svelte";
  import { state, getInfo, withdrawVested } from "./vesting";

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

  let contractAddress = "";
@@ -18,14 +18,14 @@
  let validationMessage: string | undefined = undefined;
  let valid: boolean = false;

-
  async function loadContract(config: Config) {
+
  async function loadContract(wallet: Wallet) {
    if (!valid) {
      return;
    }

    state.set("loading");
    try {
-
      info = await getInfo(contractAddress, config);
+
      info = await getInfo(contractAddress, wallet);
    } catch (error) {
      validationMessage =
        "Couldn't load contract, check dev console for details.";
@@ -105,7 +105,7 @@
          <tr>
            <td class="txt-highlight">Beneficiary</td>
            <td>
-
              <Address {config} address={info.beneficiary} compact resolve />
+
              <Address {wallet} address={info.beneficiary} compact resolve />
            </td>
          </tr>
          <tr>
@@ -145,7 +145,7 @@
          </Button>
        {:else if $state === "idle"}
          <Button
-
            on:click={() => withdrawVested(contractAddress, config)}
+
            on:click={() => withdrawVested(contractAddress, wallet)}
            variant="primary">
            Withdraw
          </Button>
@@ -176,12 +176,12 @@
        loading={$state === "loading"}
        disabled={$state === "loading"}
        on:submit={() => {
-
          loadContract(config);
+
          loadContract(wallet);
        }}
        bind:value={contractAddress} />

      <Button
-
        on:click={() => loadContract(config)}
+
        on:click={() => loadContract(wallet)}
        variant="primary"
        waiting={$state === "loading"}
        disabled={!valid || $state === "loading"}>
modified src/base/vesting/vesting.ts
@@ -1,4 +1,4 @@
-
import type { Config } from "@app/config";
+
import type { Wallet } from "@app/wallet";

import { ethers } from "ethers";
import { assert } from "@app/error";
@@ -6,6 +6,7 @@ import { writable } from "svelte/store";

import * as session from "@app/session";
import * as utils from "@app/utils";
+
import ethereumContractAbis from "@app/ethereum/contractAbis.json";

export interface VestingInfo {
  token: string;
@@ -22,16 +23,16 @@ export const state = writable<

export async function withdrawVested(
  address: string,
-
  config: Config,
+
  wallet: Wallet,
): Promise<void> {
-
  assert(config.signer);
+
  assert(wallet.signer);

  const contract = new ethers.Contract(
    address,
-
    config.abi.vesting,
-
    config.provider,
+
    ethereumContractAbis.vesting,
+
    wallet.provider,
  );
-
  const signer = config.signer;
+
  const signer = wallet.signer;

  state.set("withdrawingSign");

@@ -39,18 +40,18 @@ export async function withdrawVested(

  state.set("withdrawing");
  await tx.wait();
-
  session.state.refreshBalance(config);
+
  session.state.refreshBalance(wallet);
  state.set("withdrawn");
}

export async function getInfo(
  address: string,
-
  config: Config,
+
  wallet: Wallet,
): Promise<VestingInfo> {
  const contract = new ethers.Contract(
    address,
-
    config.abi.vesting,
-
    config.provider,
+
    ethereumContractAbis.vesting,
+
    wallet.provider,
  );
  const token = await contract.token();
  const beneficiary = await contract.beneficiary();
@@ -60,8 +61,8 @@ export async function getInfo(

  const tokenContract = new ethers.Contract(
    token,
-
    config.abi.token,
-
    config.provider,
+
    ethereumContractAbis.vesting,
+
    wallet.provider,
  );
  const symbol = await tokenContract.symbol();

modified src/components/Modal/ConnectWallet.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";

  import { createEventDispatcher } from "svelte";
  import { qrcode } from "pure-svg-code";
@@ -9,7 +9,7 @@
  import Button from "@app/Button.svelte";

  export let uri: string;
-
  export let config: Config;
+
  export let wallet: Wallet;

  $: svgString = qrcode({
    content: uri,
@@ -23,7 +23,7 @@

  const dispatch = createEventDispatcher();
  const onClickConnect = () => {
-
    state.connectMetamask(config);
+
    state.connectMetamask(wallet);
  };
  const onClose = () => {
    dispatch("close");
@@ -83,7 +83,7 @@
        variant="secondary"
        size="small"
        on:click={onClickConnect}
-
        disabled={!config.metamask.signer}>
+
        disabled={!wallet.metamask.signer}>
        Connect with Metamask
      </Button>
      <Button variant="text" size="small" on:click={onClose}>Close</Button>
modified src/components/Modal/SearchResults.svelte
@@ -2,7 +2,7 @@
  import Modal from "@app/Modal.svelte";
  import { link } from "svelte-routing";
  import { formatRadicleUrn, getSeedEmoji } from "@app/utils";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import Address from "@app/Address.svelte";
  import Button from "@app/Button.svelte";
  import { createEventDispatcher } from "svelte";
@@ -10,7 +10,7 @@

  export let query: string;
  export let results: ProjectsAndProfiles;
-
  export let config: Config;
+
  export let wallet: Wallet;

  const dispatch = createEventDispatcher();
</script>
@@ -51,8 +51,7 @@
            <a use:link href="/seeds/{project.seed.host}/{project.info.urn}">
              <span title={project.seed.host}>
                <span>
-
                  {getSeedEmoji(project.seed.host, config)}&nbsp;{project.info
-
                    .name}
+
                  {getSeedEmoji(project.seed.host)}&nbsp;{project.info.name}
                </span>
                <span class="urn">
                  &nbsp;{formatRadicleUrn(project.info.urn)}
@@ -68,7 +67,7 @@
      <ul>
        {#each results.profiles as profile}
          <li>
-
            <Address address={profile.address} {profile} {config} resolve />
+
            <Address address={profile.address} {profile} {wallet} resolve />
          </li>
        {/each}
      </ul>
modified src/config.json
@@ -1,60 +1,15 @@
{
-
  "homestead": {
-
    "registrar": {
-
      "domain": "radicle.eth",
-
      "address": "0x37723287Ae6F34866d82EE623401f92Ec9013154"
-
    },
-
    "radToken": {
-
      "address": "0x31c8EAcBFFdD875c74b94b077895Bd78CF1E64A3"
-
    },
-
    "users": {
-
      "pinned": ["cloudhead.radicle.eth"]
-
    },
-
    "reverseRegistrar": {
-
      "address": "0x084b1c3C81545d370f3634392De611CaaBFf8148"
-
    },
-
    "tokens": [],
-
    "alchemy": { "key": "cQFlLK8EokIGlJhd_soImwEyUoC7Ec8r" }
-
  },
-
  "goerli": {
-
    "registrar": {
-
      "domain": "radicle-goerli.eth",
-
      "address": "0xD88303A92577bFDF5A82FddeF342F3A27A972405"
-
    },
-
    "radToken": {
-
      "address": "0x3EE94D192397aAFAe438C9803825eb1Aa4402e09",
-
      "faucet": "0xc627191d2BB8839eAcbb7191f9500B84d201A066"
-
    },
-
    "users": {
-
      "pinned": []
-
    },
-
    "reverseRegistrar": {
-
      "address": "0xD5610A08E370051a01fdfe4bB3ddf5270af1aA48"
-
    },
-
    "tokens": [],
-
    "alchemy": { "key": "1T6h-0rxu7SRzKEtmukIoxaJOXazLDNs" }
-
  },
  "walletConnect": { "bridge": "https://radicle.bridge.walletconnect.org" },
  "reactions": ["👍", "👎", "😄", "🎉", "🙁", "🚀", "👀"],
-
  "radicle": {
-
    "seed": {
-
      "api": { "port": 8777 },
-
      "link": { "port": 8776 },
-
      "git": { "port": 443 }
-
    }
-
  },
  "seeds": {
-
    "pinned": {
-
      "willow.radicle.garden": {
+
    "pinned": [
+
      {
+
        "host": "willow.radicle.garden",
        "emoji": "🪵"
      },
-
      "pine.radicle.garden": {
-
        "emoji": "🌲"
-
      },
-
      "maple.radicle.garden": {
-
        "emoji": "🍁"
-
      }
-
    }
+
      { "host": "pine.radicle.garden", "emoji": "🌲" },
+
      { "host": "maple.radicle.garden", "emoji": "🍁" }
+
    ]
  },
  "projects": {
    "pinned": [
@@ -119,54 +74,5 @@
        "seed": "pine.radicle.garden"
      }
    ]
-
  },
-
  "ipfs": { "gateway": "https://ipfs.io/ipfs/" },
-
  "abi": {
-
    "registrar": [
-
      "function rad() view returns (address)",
-
      "function radNode() view returns (bytes32)",
-
      "function minCommitmentAge() view returns (uint256)",
-
      "function registrationFeeRad() view returns (uint256)",
-
      "function commit(bytes32)",
-
      "function commitWithPermit(bytes32, address, uint256, uint256, uint8, bytes32, bytes32)",
-
      "function register(string, address, uint256)",
-
      "function valid(string) pure returns (bool)",
-
      "function available(string) view returns (bool)"
-
    ],
-
    "token": [
-
      "function balanceOf(address) view returns (uint256)",
-
      "function approve(address, uint256) returns (bool)",
-
      "function allowance(address, address) view returns (uint256)",
-
      "function DOMAIN_SEPARATOR() view returns (bytes32)",
-
      "function name() pure returns (string)",
-
      "function symbol() pure returns (string)",
-
      "function nonces(address) view returns (uint256)"
-
    ],
-
    "resolver": [
-
      "function multicall(bytes[] calldata data) returns(bytes[] memory results)",
-
      "function setAddr(bytes32 node, address addr)",
-
      "function setText(bytes32 node, string calldata key, string calldata value)"
-
    ],
-
    "reverseRegistrar": ["function setName(string) returns (bytes32)"],
-
    "org": ["function owner() view returns (address)"],
-
    "vesting": [
-
      "function token() view returns (address)",
-
      "function totalVestingAmount() view returns (uint256)",
-
      "function vestingStartTime() view returns (uint256)",
-
      "function vestingPeriod() view returns (uint256)",
-
      "function cliffPeriod() view returns (uint256)",
-
      "function beneficiary() view returns (address)",
-
      "function interrupted() view returns (bool)",
-
      "function withdrawn() view returns (uint256)",
-
      "function withdrawableBalance() view returns (uint256)",
-
      "function withdrawVested()"
-
    ],
-
    "faucet": [
-
      "function lastWithdrawalByUser(address) view returns (uint256)",
-
      "function maxWithdrawAmount() view returns (uint256)",
-
      "function calculateTimeLock(uint256) view returns (uint256)",
-
      "function withdraw(address, uint256)"
-
    ],
-
    "ens": ["function owner(bytes32 node) view returns (address)"]
  }
}
deleted src/config.ts
@@ -1,257 +0,0 @@
-
import { get, writable } from "svelte/store";
-
import type { Writable } from "svelte/store";
-
import { ethers } from "ethers";
-
import type { TypedDataSigner } from "@ethersproject/abstract-signer";
-
import WalletConnect from "@walletconnect/client";
-
import config from "@app/config.json";
-
import { WalletConnectSigner } from "./WalletConnectSigner";
-
import { capitalize } from "@app/utils";
-

-
declare global {
-
  interface Window {
-
    // eslint-disable-next-line @typescript-eslint/naming-convention
-
    Cypress: any;
-
    ethereum: any;
-
    registrarState: any;
-
  }
-
}
-

-
export type WalletConnectState =
-
  | { state: "close" }
-
  | { state: "open"; uri: string; onClose: any };
-

-
export class Config {
-
  network: { name: string; chainId: number };
-
  registrar: { address: string; domain: string };
-
  radToken: { address: string; faucet: string };
-
  reverseRegistrar: { address: string };
-
  users: { pinned: string[] };
-
  projects: { pinned: { urn: string; name: string; seed: string }[] };
-
  seeds: { pinned: Record<string, { emoji: string }> };
-
  provider: ethers.providers.JsonRpcProvider;
-
  signer: (ethers.Signer & TypedDataSigner) | WalletConnectSigner | null;
-
  walletConnect: {
-
    client: WalletConnect;
-
    bridge: string;
-
    signer: WalletConnectSigner;
-
    state: Writable<WalletConnectState>;
-
  };
-
  metamask:
-
    | {
-
        connected: true;
-
        signer: ethers.Signer & TypedDataSigner;
-
        session: { address: string };
-
      }
-
    | {
-
        connected: false;
-
        signer: (ethers.Signer & TypedDataSigner) | null;
-
      };
-
  abi: { [contract: string]: string[] };
-
  seed: {
-
    api: { port: number };
-
    git: { port: number };
-
    link: { port: number };
-
  };
-
  tokens: string[];
-
  token: ethers.Contract;
-

-
  constructor(
-
    network: { name: string; chainId: number },
-
    provider: ethers.providers.JsonRpcProvider,
-
    metamaskSigner: (ethers.Signer & TypedDataSigner) | null,
-
  ) {
-
    const cfg = (<Record<string, any>>config)[network.name];
-

-
    const walletConnectState = writable<WalletConnectState>({ state: "close" });
-
    const wc = Config.initializeWalletConnect(
-
      config.walletConnect.bridge,
-
      walletConnectState,
-
      provider,
-
    );
-
    const metamaskSession = window.localStorage.getItem("metamask");
-
    const metamask = metamaskSession ? JSON.parse(metamaskSession) : null;
-

-
    this.network = network;
-
    this.metamask =
-
      metamask && metamaskSigner
-
        ? {
-
            connected: true,
-
            session: { address: metamask["address"] },
-
            signer: metamaskSigner,
-
          }
-
        : {
-
            connected: false,
-
            signer: metamaskSigner,
-
          };
-
    this.walletConnect = {
-
      bridge: config.walletConnect.bridge,
-
      client: wc.connector,
-
      signer: wc.signer,
-
      state: walletConnectState,
-
    };
-
    this.seed = config.radicle.seed;
-
    this.registrar = cfg.registrar;
-
    this.radToken = cfg.radToken;
-
    this.reverseRegistrar = cfg.reverseRegistrar;
-
    this.users = cfg.users;
-
    this.provider = provider;
-
    this.signer = null;
-
    this.projects = config.projects;
-
    this.seeds = config.seeds;
-
    this.abi = config.abi;
-
    this.tokens = cfg.tokens;
-
    this.token = new ethers.Contract(
-
      this.radToken.address,
-
      this.abi.token,
-
      this.provider,
-
    );
-
  }
-

-
  changeNetwork(chainId: number): void {
-
    this.network = ethers.providers.getNetwork(chainId);
-
  }
-

-
  setSigner(
-
    signer: (ethers.Signer & TypedDataSigner) | WalletConnectSigner,
-
  ): void {
-
    this.signer = signer;
-
  }
-

-
  getWalletConnectSigner(): WalletConnectSigner {
-
    if (this.walletConnect.client.connected) {
-
      this.setSigner(this.walletConnect.signer);
-
      return this.walletConnect.signer;
-
    }
-
    const wc = Config.initializeWalletConnect(
-
      this.walletConnect.bridge,
-
      this.walletConnect.state,
-
      this.provider,
-
    );
-
    this.walletConnect.client = wc.connector;
-
    this.walletConnect.signer = wc.signer;
-
    this.setSigner(wc.signer);
-

-
    return wc.signer;
-
  }
-

-
  static initializeWalletConnect(
-
    bridge: string,
-
    state: Writable<WalletConnectState>,
-
    provider: ethers.providers.JsonRpcProvider,
-
  ): {
-
    connector: WalletConnect;
-
    signer: WalletConnectSigner;
-
  } {
-
    const walletConnect = new WalletConnect({
-
      bridge,
-
      qrcodeModal: {
-
        open: (uri: string, onClose) => {
-
          state.set({ state: "open", uri, onClose });
-
        },
-
        close: () => {
-
          // We handle the "close" event through the "disconnect" handler.
-
        },
-
      },
-
    });
-
    walletConnect.on("modal_closed", () => {
-
      state.set({ state: "close" });
-
    });
-
    walletConnect.on("disconnect", () => {
-
      const wcs = get(state);
-
      if (wcs.state === "open") {
-
        wcs.onClose();
-
      }
-
    });
-

-
    // Behold, we set this private class variable here because WalletConnect doesn't
-
    // give us any other way to set it :'(
-
    //
-
    // The default is to use the favicon, which doesn't work, given that it is
-
    // designed for browsers and not mobile apps which often show a much bigger
-
    // icon, resulting in a blurry image.
-
    (walletConnect as any)._clientMeta.icons = [
-
      `${window.location.protocol}//${window.location.host}/logo.png`,
-
    ];
-

-
    const walletConnectSigner = new WalletConnectSigner(
-
      walletConnect,
-
      provider,
-
    );
-

-
    return {
-
      connector: walletConnect,
-
      signer: walletConnectSigner,
-
    };
-
  }
-
}
-

-
export function isMetamaskInstalled(): boolean {
-
  const { ethereum } = window;
-
  return Boolean(ethereum && ethereum.isMetaMask);
-
}
-

-
function getProvider(
-
  network: { name: string },
-
  config: Record<string, any>,
-
  metamask: ethers.providers.JsonRpcProvider | null,
-
): ethers.providers.JsonRpcProvider {
-
  if (metamask) {
-
    return metamask;
-
  } else if (import.meta.env.PROD) {
-
    return new ethers.providers.AlchemyWebSocketProvider(
-
      network.name,
-
      config.alchemy.key,
-
    );
-
  } else if (import.meta.env.DEV) {
-
    // The ethers defaultProvider doesn't include a `send` method, which breaks the `utils.getTokens` fn.
-
    // Since Metamask nor WalletConnect provide an `alchemy_getTokenBalances` nor `alchemy_getTokenMetadata` endpoint,
-
    // we can rely on not using `config.provider.send`.
-
    return ethers.providers.getDefaultProvider() as ethers.providers.JsonRpcProvider;
-
  } else {
-
    throw new Error("No Web3 provider available.");
-
  }
-
}
-

-
// Checks if the promise metamask.ready returns the network, else timesout after 4 seconds.
-
function checkMetaMask(
-
  metamask: ethers.providers.Web3Provider,
-
): Promise<ethers.providers.Network | null> {
-
  return new Promise(resolve => {
-
    setTimeout(() => {
-
      resolve(null);
-
    }, 4000);
-
    metamask.ready.then(network => resolve(network));
-
  });
-
}
-

-
export async function getConfig(): Promise<Config> {
-
  const metamask = isMetamaskInstalled()
-
    ? new ethers.providers.Web3Provider(window.ethereum)
-
    : null;
-
  const metamaskSigner = metamask?.getSigner() || null;
-

-
  console.debug("metamask", metamask);
-
  console.debug("metamaskSigner", metamaskSigner);
-

-
  let network = { name: "homestead", chainId: 1 };
-
  if (metamask) {
-
    // If Metamask is detected, we use the network configured there.
-
    const ready = await checkMetaMask(metamask);
-
    if (ready) network = ready;
-
  }
-

-
  const networkConfig = (<Record<string, any>>config)[network.name];
-
  if (!networkConfig) {
-
    throw new Error(
-
      `${capitalize(
-
        network.name,
-
      )} is not supported. Connect to Homestead or Goerli instead.`,
-
    );
-
  }
-

-
  const provider = getProvider(network, networkConfig, metamask);
-
  const cfg = new Config(network, provider, metamaskSigner);
-
  console.debug("config", cfg);
-

-
  return cfg;
-
}
modified src/ens/SetName.svelte
@@ -2,7 +2,7 @@
  import { createEventDispatcher } from "svelte";
  import { navigate } from "svelte-routing";
  import Modal from "@app/Modal.svelte";
-
  import type { Config } from "@app/config";
+
  import type { Wallet } from "@app/wallet";
  import { formatAddress, isAddressEqual } from "@app/utils";
  import type { User } from "@app/base/users/User";
  import ErrorModal from "@app/ErrorModal.svelte";
@@ -13,7 +13,7 @@
  const dispatch = createEventDispatcher();

  export let entity: User;
-
  export let config: Config;
+
  export let wallet: Wallet;

  enum State {
    Idle,
@@ -37,13 +37,13 @@
    }
    state = State.Checking;

-
    const domain = `${name}.${config.registrar.domain}`;
-
    const resolved = await config.provider.resolveName(domain);
+
    const domain = `${name}.${wallet.registrar.domain}`;
+
    const resolved = await wallet.provider.resolveName(domain);

    if (resolved && isAddressEqual(resolved, entity.address)) {
      try {
        state = State.Signing;
-
        const tx = await entity.setName(domain, config);
+
        const tx = await entity.setName(domain, wallet);
        state = State.Pending;
        await tx.wait();
        state = State.Success;
@@ -78,7 +78,7 @@

    <div slot="subtitle">
      The ENS name for {entity.address} was set to
-
      <span class="txt-bold">{name}.{config.registrar.domain}</span>
+
      <span class="txt-bold">{name}.{wallet.registrar.domain}</span>
      .
    </div>

@@ -90,11 +90,11 @@
  </Modal>
{:else if state === State.Mismatch}
  <ErrorModal floating title="🧣" on:close>
-
    The name <span class="txt-bold">{name}.{config.registrar.domain}</span>
+
    The name <span class="txt-bold">{name}.{wallet.registrar.domain}</span>
    does not resolve to
    <span class="txt-bold">{entity.address}</span>
-
    . Please update the ENS record for {name}.{config.registrar.domain} to point
-
    to the correct address and try again.
+
    . Please update the ENS record for {name}.{wallet.registrar.domain} to to the
+
    correct address and try again.

    <div slot="actions">
      <Button
@@ -142,7 +142,7 @@
            {valid}
            bind:value={name}>
            <svelte:fragment slot="right">
-
              .{config.registrar.domain}
+
              .{wallet.registrar.domain}
            </svelte:fragment>
          </TextInput>
        </div>
added src/ethereum/contractAbis.json
@@ -0,0 +1,48 @@
+
{
+
  "ens": ["function owner(bytes32 node) view returns (address)"],
+
  "faucet": [
+
    "function lastWithdrawalByUser(address) view returns (uint256)",
+
    "function maxWithdrawAmount() view returns (uint256)",
+
    "function calculateTimeLock(uint256) view returns (uint256)",
+
    "function withdraw(address, uint256)"
+
  ],
+
  "org": ["function owner() view returns (address)"],
+
  "registrar": [
+
    "function rad() view returns (address)",
+
    "function radNode() view returns (bytes32)",
+
    "function minCommitmentAge() view returns (uint256)",
+
    "function registrationFeeRad() view returns (uint256)",
+
    "function commit(bytes32)",
+
    "function commitWithPermit(bytes32, address, uint256, uint256, uint8, bytes32, bytes32)",
+
    "function register(string, address, uint256)",
+
    "function valid(string) pure returns (bool)",
+
    "function available(string) view returns (bool)"
+
  ],
+
  "resolver": [
+
    "function multicall(bytes[] calldata data) returns(bytes[] memory results)",
+
    "function setAddr(bytes32 node, address addr)",
+
    "function setText(bytes32 node, string calldata key, string calldata value)"
+
  ],
+
  "reverseRegistrar": ["function setName(string) returns (bytes32)"],
+
  "token": [
+
    "function balanceOf(address) view returns (uint256)",
+
    "function approve(address, uint256) returns (bool)",
+
    "function allowance(address, address) view returns (uint256)",
+
    "function DOMAIN_SEPARATOR() view returns (bytes32)",
+
    "function name() pure returns (string)",
+
    "function symbol() pure returns (string)",
+
    "function nonces(address) view returns (uint256)"
+
  ],
+
  "vesting": [
+
    "function token() view returns (address)",
+
    "function totalVestingAmount() view returns (uint256)",
+
    "function vestingStartTime() view returns (uint256)",
+
    "function vestingPeriod() view returns (uint256)",
+
    "function cliffPeriod() view returns (uint256)",
+
    "function beneficiary() view returns (address)",
+
    "function interrupted() view returns (bool)",
+
    "function withdrawn() view returns (uint256)",
+
    "function withdrawableBalance() view returns (uint256)",
+
    "function withdrawVested()"
+
  ]
+
}
added src/ethereum/networks/goerli.json
@@ -0,0 +1,16 @@
+
{
+
  "name": "goerli",
+
  "chainId": 5,
+
  "registrar": {
+
    "domain": "radicle-goerli.eth",
+
    "address": "0xD88303A92577bFDF5A82FddeF342F3A27A972405"
+
  },
+
  "radToken": {
+
    "address": "0x3EE94D192397aAFAe438C9803825eb1Aa4402e09",
+
    "faucet": "0xc627191d2BB8839eAcbb7191f9500B84d201A066"
+
  },
+
  "reverseRegistrar": {
+
    "address": "0xD5610A08E370051a01fdfe4bB3ddf5270af1aA48"
+
  },
+
  "alchemy": { "key": "1T6h-0rxu7SRzKEtmukIoxaJOXazLDNs" }
+
}
added src/ethereum/networks/homestead.json
@@ -0,0 +1,15 @@
+
{
+
  "name": "homestead",
+
  "chainId": 1,
+
  "registrar": {
+
    "domain": "radicle.eth",
+
    "address": "0x37723287Ae6F34866d82EE623401f92Ec9013154"
+
  },
+
  "radToken": {
+
    "address": "0x31c8EAcBFFdD875c74b94b077895Bd78CF1E64A3"
+
  },
+
  "reverseRegistrar": {
+
    "address": "0x084b1c3C81545d370f3634392De611CaaBFf8148"
+
  },
+
  "alchemy": { "key": "cQFlLK8EokIGlJhd_soImwEyUoC7Ec8r" }
+
}
modified src/profile.ts
@@ -7,7 +7,7 @@ import {
  identifyAddress,
  isFulfilled,
} from "@app/utils";
-
import type { Config } from "@app/config";
+
import type { Wallet } from "@app/wallet";
import { cached } from "@app/cache";
import type { Seed, InvalidSeed } from "@app/base/seeds/Seed";
import { Org } from "@app/base/orgs/Org";
@@ -107,18 +107,18 @@ export class Profile {
  private static async lookupProfile(
    addressOrName: string,
    profileType: ProfileType,
-
    config: Config,
+
    wallet: Wallet,
  ): Promise<IProfile> {
    let type = AddressType.EOA;
    let org: Org | null = null;
-
    const ens = await resolveEnsProfile(addressOrName, profileType, config);
+
    const ens = await resolveEnsProfile(addressOrName, profileType, wallet);

    if (ens) {
      if (ens.address) {
-
        type = await identifyAddress(ens.address, config);
+
        type = await identifyAddress(ens.address, wallet);

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

        return {
@@ -132,9 +132,9 @@ export class Profile {
    } else if (isAddress(addressOrName)) {
      const address = addressOrName.toLowerCase();

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

      try {
@@ -154,10 +154,10 @@ export class Profile {

  static async getMulti(
    addressesOrNames: string[],
-
    config: Config,
+
    wallet: Wallet,
  ): Promise<Profile[]> {
    const profilePromises = addressesOrNames.map(addressOrName =>
-
      this.lookupProfile(addressOrName, ProfileType.Minimal, config),
+
      this.lookupProfile(addressOrName, ProfileType.Minimal, wallet),
    );
    const profiles = await Promise.allSettled(profilePromises);
    return profiles
@@ -168,20 +168,20 @@ export class Profile {
  static async get(
    addressOrName: string,
    profileType: ProfileType,
-
    config: Config,
+
    wallet: Wallet,
  ): Promise<Profile> {
    const profile = await this.lookupProfile(
      addressOrName,
      profileType,
-
      config,
+
      wallet,
    );
    return new Profile(profile);
  }
}

export const getBalance = cached(
-
  async (address: string, config: Config) => {
-
    return await config.provider.getBalance(address);
+
  async (address: string, wallet: Wallet) => {
+
    return await wallet.provider.getBalance(address);
  },
  address => address,
  { max: 1000 },
modified src/project.ts
@@ -5,7 +5,7 @@ import type { Commit, CommitHeader, CommitsHistory } from "@app/commit";
import { isFulfilled, isOid, isRadicleId } from "@app/utils";
import { Profile, ProfileType } from "@app/profile";
import { Seed } from "@app/base/seeds/Seed";
-
import type { Config } from "@app/config";
+
import type { Wallet } from "@app/wallet";

export type Urn = string;
export type PeerId = string;
@@ -441,15 +441,15 @@ export class Project implements ProjectInfo {
    peer: string | null,
    profileName: string | null,
    seedHost: string | null,
-
    config: Config,
+
    wallet: Wallet,
  ): Promise<Project> {
    const profile = profileName
-
      ? await Profile.get(profileName, ProfileType.Project, config)
+
      ? await Profile.get(profileName, ProfileType.Project, wallet)
      : null;
    const seed = profile
      ? profile.seed
      : seedHost
-
      ? await Seed.lookup(seedHost, config)
+
      ? await Seed.lookup(seedHost)
      : null;

    if (!profile && !seed) {
modified src/session.ts
@@ -5,7 +5,7 @@ import type {
  TransactionReceipt,
  TransactionResponse,
} from "@ethersproject/providers";
-
import { Config, getConfig } from "@app/config";
+
import { Wallet, getWallet } from "@app/wallet";
import { Unreachable, assert, assertEq } from "@app/error";
import type { TypedDataSigner } from "@ethersproject/abstract-signer";
import type { WalletConnectSigner } from "./WalletConnectSigner";
@@ -55,11 +55,11 @@ export interface Session {
}

export interface Store extends Readable<State> {
-
  connectMetamask(config: Config): Promise<void>;
-
  connectWalletConnect(config: Config): Promise<void>;
+
  connectMetamask(wallet: Wallet): Promise<void>;
+
  connectWalletConnect(wallet: Wallet): Promise<void>;
  updateBalance(n: BigNumber): void;
  connectSeed(seed: { id: string; session: SeedSession }): void;
-
  refreshBalance(config: Config): Promise<void>;
+
  refreshBalance(wallet: Wallet): Promise<void>;
  setTxSigning(): void;
  setTxPending(tx: TransactionResponse): void;
  setTxConfirmed(tx: TransactionReceipt): void;
@@ -73,15 +73,15 @@ export const loadState = (initial: State): Store => {

  return {
    subscribe: store.subscribe,
-
    connectMetamask: async (config: Config) => {
-
      assert(config.metamask.signer);
-
      // We use config.metamask.signer here, because config.signer is still null on page reload.
-
      const signer = config.metamask.signer;
+
    connectMetamask: async (wallet: Wallet) => {
+
      assert(wallet.metamask.signer);
+
      // We use wallet.metamask.signer here, because wallet.signer is still null on page reload.
+
      const signer = wallet.metamask.signer;

      // Re-connect using previous session.
-
      if (config.metamask.connected) {
-
        const metamask = config.metamask.session;
-
        const tokenBalance: BigNumber = await config.token.balanceOf(
+
      if (wallet.metamask.connected) {
+
        const metamask = wallet.metamask.session;
+
        const tokenBalance: BigNumber = await wallet.token.balanceOf(
          metamask.address,
        );
        const session = {
@@ -94,7 +94,7 @@ export const loadState = (initial: State): Store => {
        };

        store.set({ connection: Connection.Connected, session });
-
        config.setSigner(signer);
+
        wallet.setSigner(signer);

        return;
      }
@@ -110,14 +110,14 @@ export const loadState = (initial: State): Store => {
      await window.ethereum.request({ method: "eth_requestAccounts" });
      const address = await signer.getAddress();

-
      config.setSigner(signer);
+
      wallet.setSigner(signer);

      try {
        // Closes the wallet modal.
        // TODO: We should move this into the session store.
-
        config.walletConnect.state.set({ state: "close" });
+
        wallet.walletConnect.state.set({ state: "close" });

-
        const tokenBalance: BigNumber = await config.token.balanceOf(address);
+
        const tokenBalance: BigNumber = await wallet.token.balanceOf(address);
        const session = {
          address,
          signer,
@@ -137,17 +137,17 @@ export const loadState = (initial: State): Store => {
      }
    },

-
    connectWalletConnect: async (config: Config) => {
+
    connectWalletConnect: async (wallet: Wallet) => {
      store.set({ connection: Connection.Connecting });
-
      // We fetch the walletConnect signer here, because config.signer is still null on page reload.
-
      const signer = config.getWalletConnectSigner();
+
      // We fetch the walletConnect signer here, because wallet.signer is still null on page reload.
+
      const signer = wallet.getWalletConnectSigner();

      try {
-
        await config.walletConnect.client.connect();
+
        await wallet.walletConnect.client.connect();
        console.debug("WalletConnect: connected.");

        const address = await signer.getAddress();
-
        const tokenBalance: BigNumber = await config.token.balanceOf(address);
+
        const tokenBalance: BigNumber = await wallet.token.balanceOf(address);
        const session = {
          address,
          signer,
@@ -161,11 +161,11 @@ export const loadState = (initial: State): Store => {
        );

        // Instead of killing the WalletConnect session, we force the UI to change network
-
        if (network.chainId !== config.network.chainId) {
-
          config.changeNetwork(network.chainId);
+
        if (network.chainId !== wallet.network.chainId) {
+
          wallet.changeNetwork(network.chainId);
        }

-
        config.walletConnect.client.on(
+
        wallet.walletConnect.client.on(
          "session_update",
          async (
            error,
@@ -178,12 +178,12 @@ export const loadState = (initial: State): Store => {
            }

            try {
-
              // We update config to reflect the new signer address.
-
              const signer = config.getWalletConnectSigner();
+
              // We update wallet to reflect the new signer address.
+
              const signer = wallet.getWalletConnectSigner();
              changeAccounts(accounts[0], signer);

              // Check the current chainId, and request Metamask to change, or reload the window to get the correct chain.
-
              if (chainId !== config.network.chainId) {
+
              if (chainId !== wallet.network.chainId) {
                if (session.signerType === SignerType.MetaMask) {
                  await window.ethereum.request({
                    method: "wallet_switchEthereumChain",
@@ -242,13 +242,13 @@ export const loadState = (initial: State): Store => {
      });
    },

-
    refreshBalance: async (config: Config) => {
+
    refreshBalance: async (wallet: Wallet) => {
      const state = get(store);
      assert(state.connection === Connection.Connected);
      const addr = state.session.address;

      try {
-
        const tokenBalance: BigNumber = await config.token.balanceOf(addr);
+
        const tokenBalance: BigNumber = await wallet.token.balanceOf(addr);

        state.session.tokenBalance = tokenBalance;
        store.set(state);
@@ -371,9 +371,9 @@ export async function changeAccounts(
  address: string,
  signer: Signer,
): Promise<void> {
-
  const config = await getConfig();
+
  const wallet = await getWallet();
  state.setChangedAccount(address, signer);
-
  state.refreshBalance(config);
+
  state.refreshBalance(wallet);
}

export function loadSeedSessions(): { [key: string]: SeedSession } {
@@ -411,17 +411,17 @@ state.subscribe(s => {
export async function approveSpender(
  spender: string,
  amount: BigNumber,
-
  config: Config,
+
  wallet: Wallet,
): Promise<void> {
-
  assert(config.signer);
+
  assert(wallet.signer);

-
  const signer = config.signer;
+
  const signer = wallet.signer;
  const addr = await signer.getAddress();

-
  const allowance = await config.token.allowance(addr, spender);
+
  const allowance = await wallet.token.allowance(addr, spender);

  if (allowance < amount) {
-
    const tx = await config.token.connect(signer).approve(spender, amount);
+
    const tx = await wallet.token.connect(signer).approve(spender, amount);
    await tx.wait();
  }
}
@@ -431,9 +431,9 @@ export function disconnectMetamask(): void {
  window.location.reload();
}

-
export function disconnectWallet(config: Config): void {
-
  if (config.walletConnect.client.connected) {
-
    config.walletConnect.client.killSession();
+
export function disconnectWallet(wallet: Wallet): void {
+
  if (wallet.walletConnect.client.connected) {
+
    wallet.walletConnect.client.killSession();
  }
  disconnectMetamask();
}
@@ -449,7 +449,7 @@ function saveMetamaskSession(session: Session): void {
      address: session.address,
      tokenBalance: null,
      tx: null,
-
      config: null,
+
      wallet: null,
    }),
  );
}
modified src/siwe.ts
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { SiweMessage } from "siwe";
import { Request, type Host } from "@app/api";
-
import type { Config } from "@app/config";
+
import type { Wallet } from "@app/wallet";
import { connectSeed } from "@app/session";
import type { Seed } from "@app/base/seeds/Seed";

@@ -22,7 +22,7 @@ export function createSiweMessage(
  seed: Seed,
  address: string,
  nonce: string,
-
  config: Config,
+
  wallet: Wallet,
): string {
  const nextWeek = new Date();
  nextWeek.setDate(nextWeek.getDate() + 7);
@@ -35,7 +35,7 @@ export function createSiweMessage(
    nonce,
    version: "1",
    expirationTime: nextWeek.toISOString(),
-
    chainId: config.network.chainId,
+
    chainId: wallet.network.chainId,
  });

  return message.prepareMessage();
@@ -50,16 +50,16 @@ export async function createUnauthorizedSession(
/// Signs the user into given seed and returns when successfull a session id
export async function signInWithEthereum(
  seed: Seed,
-
  config: Config,
+
  wallet: Wallet,
): Promise<{ id: string } | null> {
-
  if (!config.signer) {
+
  if (!wallet.signer) {
    return null;
  }

-
  const address = await config.signer.getAddress();
+
  const address = await wallet.signer.getAddress();
  const result = await createUnauthorizedSession(seed.api);
-
  const message = createSiweMessage(seed, address, result.nonce, config);
-
  const signature = await config.signer.signMessage(message);
+
  const message = createSiweMessage(seed, address, result.nonce, wallet);
+
  const signature = await wallet.signer.signMessage(message);

  const auth: {
    id: string;
modified src/utils.test.ts
@@ -1,6 +1,7 @@
+
import type { Wallet } from "@app/wallet";
+

import { BigNumber } from "ethers";
import { describe, expect, test } from "vitest";
-
import type { Config } from "./config";
import * as utils from "./utils";

describe("Conversions", () => {
@@ -174,7 +175,7 @@ describe("Others", () => {
        network: {
          name,
        },
-
      } as Config),
+
      } as Wallet),
    ).toEqual(expected);
  });
});
@@ -190,7 +191,7 @@ describe("Parse Strings", () => {
          address: "0x1234567890123456789012345678901234567890",
          domain: "radicle.eth",
        },
-
      } as Config),
+
      } as Wallet),
    ).toEqual(expected);
  });

modified src/utils.ts
@@ -3,7 +3,7 @@ import type { RouteLocation } from "@app/index";
import md5 from "md5";
import { BigNumber } from "ethers";
import katex from "katex";
-
import type { Config } from "@app/config";
+
import type { Wallet } from "@app/wallet";
import { assert } from "@app/error";
import type { EnsProfile } from "@app/base/registrations/registrar";
import { getAddress, getResolver } from "@app/base/registrations/registrar";
@@ -17,6 +17,7 @@ import { parseUnits } from "@ethersproject/units";
import * as cache from "@app/cache";
import type { marked } from "marked";
import emojis from "@app/emojis";
+
import config from "@app/config.json";

export enum AddressType {
  Contract,
@@ -48,9 +49,9 @@ export type State =
export async function isReverseRecordSet(
  address: string,
  domain: string,
-
  config: Config,
+
  wallet: Wallet,
): Promise<boolean> {
-
  const name = await lookupAddress(address, config);
+
  const name = await lookupAddress(address, wallet);
  return name === domain;
}

@@ -128,11 +129,11 @@ export function formatCommit(oid: string): string {
  return oid.substring(0, 7);
}

-
export function formatProfile(input: string, config: Config): string {
+
export function formatProfile(input: string, wallet: Wallet): string {
  if (isAddress(input)) {
    return ethers.utils.getAddress(input);
  } else {
-
    return parseEnsLabel(input, config);
+
    return parseEnsLabel(input, wallet);
  }
}

@@ -142,8 +143,8 @@ export function capitalize(s: string): string {
}

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

  return label;
@@ -264,8 +265,8 @@ export function isUrl(input: string): boolean {
  return /^https?:\/\//.test(input);
}

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

// Get the explorer link of an address, eg. Etherscan.
-
export function explorerLink(addr: string, config: Config): string {
-
  if (config.network.name === "goerli") {
+
export function explorerLink(addr: string, wallet: Wallet): string {
+
  if (wallet.network.name === "goerli") {
    return `https://goerli.etherscan.io/address/${addr}`;
  }
  return `https://etherscan.io/address/${addr}`;
}

// Format a name.
-
export function formatName(input: string, config: Config): string {
-
  return parseEnsLabel(input, config);
+
export function formatName(input: string, wallet: Wallet): string {
+
  return parseEnsLabel(input, wallet);
}

// Parse a Radicle Id (URN).
@@ -321,22 +322,24 @@ export function parseEmoji(input: string): string {
  return input;
}

-
// Fetch from config the emoji to the corresponding pinned seed, if non found return default emoji
-
// @dev: This helper fn lets us get a seed emoji quick without multiLookups or complex type usage.
-
// TODO: Should be revisited, when we have a stable implementation for seed avatars.
-
export function getSeedEmoji(input: string, config: Config): string {
-
  if (config.seeds.pinned[input]) {
-
    return config.seeds.pinned[input].emoji;
+
export function getSeedEmoji(seedHost: string): string {
+
  const seed = config.seeds.pinned.find(s => s.host === seedHost);
+

+
  if (seed) {
+
    return seed.emoji;
+
  } else if (isLocal(seedHost)) {
+
    return "🏠";
+
  } else {
+
    return "🌱";
  }
-
  return "🌱";
}

// Identify an address by checking whether it's a contract or an externally-owned address.
export async function identifyAddress(
  address: string,
-
  config: Config,
+
  wallet: Wallet,
): Promise<AddressType> {
-
  const code = await getCode(address, config);
+
  const code = await getCode(address, wallet);
  const bytes = ethers.utils.arrayify(code);

  if (bytes.length > 0) {
@@ -349,25 +352,25 @@ export async function identifyAddress(
export async function resolveEnsProfile(
  addressOrName: string,
  profileType: ProfileType,
-
  config: Config,
+
  wallet: Wallet,
): Promise<EnsProfile | null> {
  const name = ethers.utils.isAddress(addressOrName)
-
    ? await lookupAddress(addressOrName, config)
+
    ? await lookupAddress(addressOrName, wallet)
    : addressOrName;

  if (name) {
-
    const resolver = await getResolver(name, config);
+
    const resolver = await getResolver(name, wallet);
    if (!resolver) {
      return null;
    }

    if (profileType === ProfileType.Full) {
-
      const registration = await getRegistration(name, config, resolver);
+
      const registration = await getRegistration(name, wallet, resolver);
      if (registration) {
        return registration.profile;
      }
    } else {
-
      const promises: [Promise<any>] = [getAvatar(name, config, resolver)];
+
      const promises: [Promise<any>] = [getAvatar(name, wallet, resolver)];

      if (addressOrName === name) {
        promises.push(getAddress(resolver));
@@ -376,7 +379,7 @@ export async function resolveEnsProfile(
      }

      if (profileType === ProfileType.Project) {
-
        promises.push(getSeed(name, config, resolver));
+
        promises.push(getSeed(name, wallet, resolver));
      } else if (profileType === ProfileType.Minimal) {
        promises.push(Promise.resolve(null));
      }
@@ -401,12 +404,12 @@ export async function resolveEnsProfile(
// Get token balances for an address.
export async function getTokens(
  address: string,
-
  config: Config,
+
  wallet: Wallet,
): Promise<Array<Token>> {
  const userBalances = await getRpcMethod(
    "alchemy_getTokenBalances",
    [address, "DEFAULT_TOKENS"],
-
    config,
+
    wallet,
  );
  const balances = userBalances.tokenBalances
    .filter((token: any) => {
@@ -421,7 +424,7 @@ export async function getTokens(
      const tokenMetaData = await getRpcMethod(
        "alchemy_getTokenMetadata",
        [token.contractAddress],
-
        config,
+
        wallet,
      );
      return { ...tokenMetaData, balance: BigNumber.from(token.tokenBalance) };
    });
@@ -430,8 +433,8 @@ export async function getTokens(
}

export const getRpcMethod = cache.cached(
-
  async (method: string, props: string[], config: Config) => {
-
    return await config.provider.send(method, props);
+
  async (method: string, props: string[], wallet: Wallet) => {
+
    return await wallet.provider.send(method, props);
  },
  (method, props) => JSON.stringify([method, props]),
  { ttl: 2 * 60 * 1000, max: 1000 },
@@ -465,16 +468,16 @@ export function gravatarURL(email: string): string {
}

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

export const lookupAddress = cache.cached(
-
  async (address: string, config: Config) => {
-
    return await config.provider.lookupAddress(address);
+
  async (address: string, wallet: Wallet) => {
+
    return await wallet.provider.lookupAddress(address);
  },
  address => address,
  { max: 1000 },
added src/wallet.ts
@@ -0,0 +1,264 @@
+
import type { TypedDataSigner } from "@ethersproject/abstract-signer";
+
import type { Writable } from "svelte/store";
+

+
import WalletConnect from "@walletconnect/client";
+
import { ethers } from "ethers";
+
import { get, writable } from "svelte/store";
+

+
import { WalletConnectSigner } from "@app/WalletConnectSigner";
+
import { capitalize } from "@app/utils";
+
import ethereumContractAbis from "@app/ethereum/contractAbis.json";
+
import homestead from "@app/ethereum/networks/homestead.json";
+
import goerli from "@app/ethereum/networks/goerli.json";
+
import config from "@app/config.json";
+

+
interface NetworkConfig {
+
  name: string;
+
  chainId: number;
+
  registrar: {
+
    domain: string;
+
    address: string;
+
  };
+
  radToken: {
+
    address: string;
+
    faucet?: string;
+
  };
+
  reverseRegistrar: {
+
    address: string;
+
  };
+
  alchemy: { key: string };
+
}
+

+
declare global {
+
  interface Window {
+
    // eslint-disable-next-line @typescript-eslint/naming-convention
+
    Cypress: any;
+
    ethereum: any;
+
    registrarState: any;
+
  }
+
}
+

+
export type WalletConnectState =
+
  | { state: "close" }
+
  | { state: "open"; uri: string; onClose: any };
+

+
export class Wallet {
+
  network: { name: string; chainId: number };
+
  registrar: { address: string; domain: string };
+
  radToken: { address: string; faucet?: string };
+
  reverseRegistrar: { address: string };
+
  provider: ethers.providers.JsonRpcProvider;
+
  signer: (ethers.Signer & TypedDataSigner) | WalletConnectSigner | null;
+
  walletConnect: {
+
    client: WalletConnect;
+
    bridge: string;
+
    signer: WalletConnectSigner;
+
    state: Writable<WalletConnectState>;
+
  };
+
  metamask:
+
    | {
+
        connected: true;
+
        signer: ethers.Signer & TypedDataSigner;
+
        session: { address: string };
+
      }
+
    | {
+
        connected: false;
+
        signer: (ethers.Signer & TypedDataSigner) | null;
+
      };
+
  token: ethers.Contract;
+

+
  constructor(
+
    network: NetworkConfig,
+
    provider: ethers.providers.JsonRpcProvider,
+
    metamaskSigner: (ethers.Signer & TypedDataSigner) | null,
+
  ) {
+
    const walletConnectState = writable<WalletConnectState>({ state: "close" });
+
    const wc = Wallet.initializeWalletConnect(
+
      config.walletConnect.bridge,
+
      walletConnectState,
+
      provider,
+
    );
+
    const metamaskSession = window.localStorage.getItem("metamask");
+
    const metamask = metamaskSession ? JSON.parse(metamaskSession) : null;
+

+
    this.network = network;
+
    this.metamask =
+
      metamask && metamaskSigner
+
        ? {
+
            connected: true,
+
            session: { address: metamask["address"] },
+
            signer: metamaskSigner,
+
          }
+
        : {
+
            connected: false,
+
            signer: metamaskSigner,
+
          };
+
    this.walletConnect = {
+
      bridge: config.walletConnect.bridge,
+
      client: wc.connector,
+
      signer: wc.signer,
+
      state: walletConnectState,
+
    };
+
    this.registrar = network.registrar;
+
    this.radToken = network.radToken;
+
    this.reverseRegistrar = network.reverseRegistrar;
+
    this.provider = provider;
+
    this.signer = null;
+
    this.token = new ethers.Contract(
+
      this.radToken.address,
+
      ethereumContractAbis.token,
+
      this.provider,
+
    );
+
  }
+

+
  changeNetwork(chainId: number): void {
+
    this.network = ethers.providers.getNetwork(chainId);
+
  }
+

+
  setSigner(
+
    signer: (ethers.Signer & TypedDataSigner) | WalletConnectSigner,
+
  ): void {
+
    this.signer = signer;
+
  }
+

+
  getWalletConnectSigner(): WalletConnectSigner {
+
    if (this.walletConnect.client.connected) {
+
      this.setSigner(this.walletConnect.signer);
+
      return this.walletConnect.signer;
+
    }
+
    const wc = Wallet.initializeWalletConnect(
+
      this.walletConnect.bridge,
+
      this.walletConnect.state,
+
      this.provider,
+
    );
+
    this.walletConnect.client = wc.connector;
+
    this.walletConnect.signer = wc.signer;
+
    this.setSigner(wc.signer);
+

+
    return wc.signer;
+
  }
+

+
  static initializeWalletConnect(
+
    bridge: string,
+
    state: Writable<WalletConnectState>,
+
    provider: ethers.providers.JsonRpcProvider,
+
  ): {
+
    connector: WalletConnect;
+
    signer: WalletConnectSigner;
+
  } {
+
    const walletConnect = new WalletConnect({
+
      bridge,
+
      qrcodeModal: {
+
        open: (uri: string, onClose) => {
+
          state.set({ state: "open", uri, onClose });
+
        },
+
        close: () => {
+
          // We handle the "close" event through the "disconnect" handler.
+
        },
+
      },
+
    });
+
    walletConnect.on("modal_closed", () => {
+
      state.set({ state: "close" });
+
    });
+
    walletConnect.on("disconnect", () => {
+
      const wcs = get(state);
+
      if (wcs.state === "open") {
+
        wcs.onClose();
+
      }
+
    });
+

+
    // Behold, we set this private class variable here because WalletConnect doesn't
+
    // give us any other way to set it :'(
+
    //
+
    // The default is to use the favicon, which doesn't work, given that it is
+
    // designed for browsers and not mobile apps which often show a much bigger
+
    // icon, resulting in a blurry image.
+
    (walletConnect as any)._clientMeta.icons = [
+
      `${window.location.protocol}//${window.location.host}/logo.png`,
+
    ];
+

+
    const walletConnectSigner = new WalletConnectSigner(
+
      walletConnect,
+
      provider,
+
    );
+

+
    return {
+
      connector: walletConnect,
+
      signer: walletConnectSigner,
+
    };
+
  }
+
}
+

+
export function isMetamaskInstalled(): boolean {
+
  const { ethereum } = window;
+
  return Boolean(ethereum && ethereum.isMetaMask);
+
}
+

+
function getProvider(
+
  networkConfig: NetworkConfig,
+
  metamask: ethers.providers.JsonRpcProvider | null,
+
): ethers.providers.JsonRpcProvider {
+
  if (metamask) {
+
    return metamask;
+
  } else if (import.meta.env.PROD) {
+
    return new ethers.providers.AlchemyWebSocketProvider(
+
      networkConfig.name,
+
      networkConfig.alchemy.key,
+
    );
+
  } else if (import.meta.env.DEV) {
+
    // The ethers defaultProvider doesn't include a `send` method, which breaks the `utils.getTokens` fn.
+
    // Since Metamask nor WalletConnect provide an `alchemy_getTokenBalances` nor `alchemy_getTokenMetadata` endpoint,
+
    // we can rely on not using `config.provider.send`.
+
    return ethers.providers.getDefaultProvider() as ethers.providers.JsonRpcProvider;
+
  } else {
+
    throw new Error("No Web3 provider available.");
+
  }
+
}
+

+
// Checks if the promise metamask.ready returns the network, else timesout after 4 seconds.
+
function checkMetaMask(
+
  metamask: ethers.providers.Web3Provider,
+
): Promise<ethers.providers.Network | null> {
+
  return new Promise(resolve => {
+
    setTimeout(() => {
+
      resolve(null);
+
    }, 4000);
+
    metamask.ready.then(network => resolve(network));
+
  });
+
}
+

+
export async function getWallet(): Promise<Wallet> {
+
  const metamask = isMetamaskInstalled()
+
    ? new ethers.providers.Web3Provider(window.ethereum)
+
    : null;
+
  const metamaskSigner = metamask?.getSigner() || null;
+

+
  let selectedNetwork: NetworkConfig;
+

+
  if (metamask) {
+
    const ready = await checkMetaMask(metamask);
+
    if (ready) {
+
      if (ready.name === "homestead") {
+
        selectedNetwork = homestead;
+
      } else if (ready.name === "goerli") {
+
        selectedNetwork = goerli;
+
      } else {
+
        throw new Error(
+
          `${capitalize(
+
            ready.name,
+
          )} is not supported. Connect to Homestead or Goerli instead.`,
+
        );
+
      }
+
    } else {
+
      throw new Error("Metamask was not ready after 4 seconds.");
+
    }
+
  } else {
+
    // Fall back to homestead.
+
    selectedNetwork = homestead;
+
  }
+

+
  const provider = getProvider(selectedNetwork, metamask);
+
  const cfg = new Wallet(selectedNetwork, provider, metamaskSigner);
+

+
  return cfg;
+
}