Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Improve vesting discovery and error messages
Sebastian Martinez committed 3 years ago
commit b2c82b06bdada4e33bdb06a6c56afeb6038fe19e
parent 8df7d664194bdaee31194e8ab0fb08b5cf3d042a
12 files changed +491 -323
modified src/App.svelte
@@ -105,7 +105,7 @@
          session={$session}
          activeRoute={$activeRouteStore} />
      {:else if $activeRouteStore.resource === "vesting"}
-
        <Vesting {wallet} session={$session} activeRoute={$activeRouteStore} />
+
        <Vesting {wallet} activeRoute={$activeRouteStore} />
      {:else if $activeRouteStore.resource === "projects"}
        <Projects {wallet} activeRoute={$activeRouteStore} />
      {:else if $activeRouteStore.resource === "profile"}
modified src/components/Address.svelte
@@ -9,7 +9,6 @@
  import Link from "@app/components/Link.svelte";
  import {
    AddressType,
-
    explorerLink,
    formatAddress,
    identifyAddress,
    parseEnsLabel,
@@ -65,12 +64,6 @@
    align-items: center;
    height: 100%;
  }
-
  .address a {
-
    color: var(--color-foreground-6);
-
  }
-
  .address a:hover {
-
    color: var(--color-foreground);
-
  }
  .highlight {
    color: var(--color-foreground-6);
    font-weight: var(--font-weight-bold);
@@ -96,37 +89,18 @@
    {/if}
  {/if}
  <div class="wrapper">
-
    {#if addressType === AddressType.Org}
-
      <Link
-
        route={{
-
          resource: "profile",
-
          params: { addressOrName: addressOrName },
-
        }}>
-
        {addressLabel}
-
      </Link>
-
      {#if !noBadge}
+
    <Link route={{ resource: "profile", params: { addressOrName } }}>
+
      {addressLabel}
+
    </Link>
+

+
    {#if !noBadge}
+
      {#if addressType === AddressType.Org}
        <Badge variant="foreground">org</Badge>
-
      {/if}
-
    {:else if addressType === AddressType.Contract}
-
      <Link route={{ resource: "profile", params: { addressOrName: address } }}>
-
        {addressLabel}
-
      </Link>
-
      {#if !noBadge}
+
      {:else if addressType === AddressType.Vesting}
+
        <Badge variant="foreground">vesting contract</Badge>
+
      {:else if addressType === AddressType.Contract}
        <Badge variant="foreground">contract</Badge>
      {/if}
-
    {:else if addressType === AddressType.EOA}
-
      <Link
-
        route={{
-
          resource: "profile",
-
          params: { addressOrName: addressOrName },
-
        }}>
-
        {addressLabel}
-
      </Link>
-
    {:else}
-
      <!-- While we're waiting to find out what address type it is -->
-
      <a href={explorerLink(address, wallet)} target="_blank" rel="noreferrer">
-
        {addressLabel}
-
      </a>
    {/if}
  </div>
</div>
modified src/components/Badge.svelte
@@ -16,6 +16,7 @@
    line-height: 1.6;
    height: var(--button-tiny-height);
    display: flex;
+
    white-space: nowrap;
  }
  .foreground {
    color: var(--color-foreground-6);
modified src/components/Icon.svelte
@@ -15,6 +15,7 @@
    | "gear"
    | "chevron-down"
    | "chevron-up"
+
    | "etherscan"
    | "url";
</script>

@@ -59,6 +60,42 @@
    14.7859 16.9725C14.8779 16.9315 14.9607 16.8724 15.0293 16.7987L19.2793
    12.5487C19.4198 12.4081 19.4987 12.2175 19.4987 12.0187C19.4987 11.82
    19.4198 11.6293 19.2793 11.4887L15.0293 7.23871V7.23871Z" />
+
  {:else if name === "etherscan"}
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="m12.001 4.8414c-3.9536 0-7.1586 3.205-7.1586 7.1586 0 1.4947
+
    0.45765 2.8814 1.2405 4.029 0.14846 0.0072 0.29643 0.01103 0.44383
+
    0.01179v-4.8829c0-0.23253 0.18853-0.42109 0.42109-0.42109h1.6844c0.23256 0
+
    0.42109 0.18857 0.42109 0.42109v4.6036c0.28408-0.06325 0.56493-0.13618
+
    0.84219-0.21771v-6.0703c0-0.23256 0.18853-0.42109
+
    0.42109-0.42109h1.6844c0.23253 0 0.42109 0.18853 0.42109
+
    0.42109v5.066c0.28752-0.14351 0.56839-0.29359
+
    0.84219-0.44855v-6.3019c0-0.23256 0.18857-0.42109
+
    0.4211-0.42109h1.6844c0.23253 0 0.42109 0.18853 0.42109
+
    0.42109v4.6166c1.5372-1.1835 2.6045-2.3046
+
    2.9851-2.7241-0.96346-2.8161-3.6334-4.8402-6.775-4.8402zm7.3852
+
    5.4209c-0.47314 0.51981-1.8267 1.9326-3.7716 3.3228-0.27337
+
    0.19539-0.666-0.0066-0.666-0.34252v-5.0324h-0.84219v6.1245c0 0.14982-0.07967
+
    0.28837-0.20912 0.36383-0.54498 0.3175-1.1208 0.61909-1.723 0.8912-0.27388
+
    0.12372-0.5945-0.08329-0.5945-0.38379v-5.3114h-0.84219v5.9606c0
+
    0.18334-0.11866 0.34563-0.29334 0.4013-0.56266 0.17913-1.1429 0.32668-1.7377
+
    0.43356-0.24549
+
    0.04413-0.49555-0.16507-0.49555-0.41444v-4.6966h-0.84219v4.877c0
+
    0.22714-0.18015 0.41335-0.40718 0.42084-0.32874
+
    0.01086-0.58124-0.01095-0.90874-0.01668-0.42767
+
    0-0.63194-0.28744-0.85625-0.64916-0.75855-1.2233-1.1966-2.6665-1.1966-4.2109
+
    0-4.4187 3.5821-8.0008 8.0008-8.0008 3.4905 0 6.458 2.2349 7.5514 5.351
+
    0.13374 0.38113 0.09955 0.62011-0.16616 0.91215zm0.31068 0.92928c0.24558
+
    0.07765 0.29216 0.3165 0.29932 0.54043 0.14132 4.4581-3.5249 8.2688-7.9952
+
    8.2688-1.6352
+
    0-3.157-0.491-4.4245-1.3338-0.15447-0.10266-0.22354-0.29434-0.17006-0.47205
+
    0.053471-0.17762 0.2169-0.29931 0.4024-0.29965 3.1395-0.0061 5.924-1.539
+
    7.9792-3.1492 1.8644-1.46063.0905-2.9551 3.3867-3.3309 0.13046-0.16541
+
    0.29544-0.29544 0.52216-0.22369zm-10.399 7.4391c0.83386 0.34033 1.7464
+
    0.52788 2.7033 0.52788 3.689 0 6.7262-2.7903 7.1162-6.3755-0.62086
+
    0.68883-1.5821 1.6643-2.8098 2.626-1.8078 1.4163-4.2267 2.8293-7.0097
+
    3.2216z" />
  {:else if name === "clipboard"}
    <path
      d="M9 5H14.7071L18 8.29289V17H9V5ZM10 6V16H17V9H14V6H10ZM15
@@ -198,22 +235,41 @@
    17.25Z" />
  {:else if name === "github"}
    <path
-
      d="M12 4C7.58 4 4 7.67295 4 12.2031C4 15.8282 6.292 18.9023 9.47
-
    19.9858C9.87 20.0631 10.0167 19.8095 10.0167 19.5914C10.0167 19.3966 10.01
-
    18.8805 10.0067 18.1969C7.78133 18.6918 7.312 17.0963 7.312 17.0963C6.948
-
    16.1495 6.422 15.8966 6.422 15.8966C5.69733 15.388 6.478 15.3982 6.478
-
    15.3982C7.28133 15.4557 7.70333 16.2432 7.70333 16.2432C8.41667 17.4975
-
    9.576 17.1352 10.0333 16.9254C10.1053 16.3949 10.3113 16.0333 10.54
-
    15.8282C8.76333 15.6231 6.896 14.9177 6.896 11.7745C6.896 10.879 7.206
-
    10.1476 7.71933 9.57334C7.62933 9.36621 7.35933 8.53222 7.78933
-
    7.40224C7.78933 7.40224 8.45933 7.18213 9.98933 8.24306C10.6293 8.06054
-
    11.3093 7.97031 11.9893 7.96621C12.6693 7.97031 13.3493 8.06054 13.9893
-
    8.24306C15.5093 7.18213 16.1793 7.40224 16.1793 7.40224C16.6093 8.53222
-
    16.3393 9.36621 16.2593 9.57334C16.7693 10.1476 17.0793 10.879 17.0793
-
    11.7745C17.0793 14.9259 15.2093 15.6197 13.4293 15.8214C13.7093 16.0675
-
    13.9693 16.5706 13.9693 17.339C13.9693 18.4368 13.9593 19.3186 13.9593
-
    19.5852C13.9593 19.8006 14.0993 20.0569 14.5093 19.9749C17.71 18.8989 20
-
    15.8227 20 12.2031C20 7.67295 16.418 4 12 4" />
+
      clip-rule="evenodd"
+
      fill-rule="evenodd"
+
      d="m7.3295 4.5202c-0.26315-0.090455-0.55523 0.13567-0.52819
+
  0.41659l0.22799 3.0462c-0.34577 0.3331-1.0298 1.2253-1.0298 2.873 0 1.7391
+
  0.87721 2.9043 2.1775 3.6113 0.30568 0.16625 0.63459 0.3073 0.98108
+
  0.42571l-0.49322
+
  0.9844-1.2994-0.10729c-0.4524-0.037363-0.87865-0.22746-1.2087-0.53908l-1.0828-1.0224c-0.16064-0.15169-0.41382-0.14441-0.5655
+
  0.01616-0.15168 0.16065-0.14442 0.41387 0.016209 0.56548l1.0828 1.0225c0.46207
+
  0.43628 1.0588 0.70246 1.6922 0.7547l0.97925 0.08089c-0.18373 0.37987-0.27927
+
  0.79662-0.27927 1.219v0.63245c0 0.2209-0.1791 0.40003-0.40003
+
  0.40003h-0.40003c-0.22093 0-0.40003 0.17905-0.40003 0.40003 0 0.2209 0.1791
+
  0.40003 0.40003 0.40003h0.40003c0.6628 0 1.2001-0.53732
+
  1.2001-1.2001v-0.63245c0-0.31106 0.072566-0.61789
+
  0.21192-0.89599l0.92927-1.8546c0.40628 0.09553 0.82889 0.16585 1.2603
+
  0.21426l-0.3999 4.7353c-0.01859 0.2201 0.1448 0.41363 0.36495 0.43228 0.22016
+
  0.01856 0.4137-0.14481 0.43226-0.36499l0.40003-4.7369 8e-5 -9.6e-4c0.53228
+
  0.02768 1.0706 0.02768 1.6028 0l8e-5 9.6e-4 0.40003 4.7369c0.01856 0.22018
+
  0.2121 0.38355 0.43228 0.36499 0.2201-0.01864 0.38355-0.21218
+
  0.36491-0.43228l-0.39987-4.7353c0.39443-0.04424 0.7815-0.10681
+
  1.1555-0.19026l0.99776 1.8113c0.16289 0.29554 0.24826 0.62757 0.24826
+
  0.96504v0.58277c0 0.66277 0.53732 1.2001 1.2001 1.2001h0.40003c0.2209 0
+
  0.40003-0.17914 0.40003-0.40003
+
  0-0.22098-0.17913-0.40003-0.40003-0.40003h-0.40003c-0.2209
+
  0-0.40003-0.17913-0.40003-0.40003v-0.58277c0-0.47244-0.11953-0.93728-0.34755-1.3511l-0.90431-1.6416c0.38083-0.12465
+
  0.74142-0.27586 1.0745-0.45692 1.3003-0.70702 2.1775-1.8722 2.1775-3.6113
+
  0-1.6477-0.68398-2.5399-1.0298-2.873l0.22794-3.0454c0.027042-0.27529-0.26266-0.50894-0.52844-0.41737l-3.0828
+
  0.98573c-0.74062-0.18142-1.5924-0.27064-2.3875-0.27064-0.79502 0-1.6469
+
  0.089215-2.3874 0.27064zm0.5121 3.5965c0.012201 0.12941-0.037451
+
  0.25373-0.13439 0.3386-0.1738 0.1528-0.32207 0.31325-0.44444 0.51351-0.22285
+
  0.36466-0.46318 0.96791-0.46318 1.8872 0 3.1088 3.5475 3.7601 6.0005 3.7601
+
  2.4529 0 6.0005-0.65125 6.0005-3.7601
+
  0-1.7223-0.81855-2.3326-0.87087-2.372-0.11889-0.082175-0.18474-0.22163-0.17121-0.36685l0.1989-2.6568-2.6353
+
  0.8426c-0.07233 0.023138-0.14977 0.025194-0.2233
+
  0.00593-0.68854-0.1805-1.5179-0.27343-2.2987-0.27343s-1.6102 0.092928-2.2987
+
  0.27343c-0.07348 0.019266-0.15092 0.017209-0.22327-0.00593l-2.6353-0.8426z" />
  {:else if name === "moon"}
    <path
      fill-rule="evenodd"
@@ -238,23 +294,25 @@
    17.5858 7.5 18 7.5H19.5V6C19.5 5.58579 19.8358 5.25 20.25 5.25Z" />
  {:else if name === "url"}
    <path
-
      d="M18.7566 11.2493L15.7531 14.2518C14.0953 15.9107 11.4059 15.9107
-
    9.74803 14.2518C9.48676 13.9916 9.28252 13.6982 9.10313 13.3954L10.4987
-
    11.9999C10.565 11.933 10.6469 11.8947 10.7252 11.8496C10.8216 12.1793
-
    10.9901 12.4914 11.2493 12.7505C12.0772 13.5789 13.4245 13.5779 14.2518
-
    12.7505L17.2543 9.74802C18.0827 8.91963 18.0827 7.57285 17.2543
-
    6.74499C16.427 5.91713 15.0802 5.91713 14.2518 6.74499L13.1839
-
    7.81391C12.3177 7.47644 11.3841 7.38573 10.4753 7.51894L12.7505
-
    5.24373C14.4094 3.58542 17.0977 3.58542 18.7566 5.24373C20.4145 6.90211
-
    20.4145 9.591 18.7566 11.2493ZM10.8164 16.1865L9.74803 17.2554C8.92016
-
    18.0828 7.57284 18.0828 6.74497 17.2554C5.9171 16.427 5.9171 15.0802
-
    6.74497 14.2519L9.74803 11.2493C10.5764 10.421 11.9227 10.421 12.7506
-
    11.2493C13.0092 11.508 13.1777 11.8201 13.2752 12.1493C13.354 12.1036
-
    13.4349 12.0663 13.5012 12L14.8967 10.605C14.7184 10.3012 14.5131 10.0088
-
    14.2518 9.74809C12.594 8.08978 9.90462 8.08978 8.24627 9.74809L5.24374
-
    12.7506C3.58542 14.4094 3.58542 17.0978 5.24374 18.7566C6.90207 20.4145
-
    9.59097 20.4145 11.2493 18.7566L13.5251 16.4809C12.6158 16.6146 11.6822
-
    16.5234 10.8164 16.1865Z" />
+
      clip-rule="evenodd"
+
      fill-rule="evenodd"
+
      d="m12.671 11.04c-0.67865-0.74752-1.0922-1.7401-1.0922-2.8293 0-2.3254
+
  1.8851-4.2105 4.2105-4.2105 2.3254 0 4.2105 1.8851 4.2105 4.2105 0
+
  2.3254-1.8851 4.2105-4.2105 4.2105-0.93263
+
  0-1.7944-0.30324-2.4921-0.81642l-1.6927 1.6927c0.51318 0.69777 0.81642 1.5595
+
  0.81642 2.4921 0 2.3254-1.8851 4.2105-4.2105 4.2105-2.3254
+
  0-4.2105-1.8851-4.2105-4.2105 0-2.3254 1.8851-4.2105 4.2105-4.2105 1.0892 0
+
  2.0818 0.41356 2.8293 1.0922zm-0.2501-2.8293c0-1.8603 1.5081-3.3684
+
  3.3684-3.3684 1.8603 0 3.3684 1.5081 3.3684 3.3684 0 1.8603-1.5081
+
  3.3684-3.3684 3.3684-0.69962
+
  0-1.3495-0.2133-1.888-0.57844l1.2291-1.2291c0.16446-0.16443 0.16446-0.43102
+
  0-0.59545-0.16438-0.16444-0.43099-0.16444-0.59545 0l-1.2675
+
  1.2676c-0.52674-0.59443-0.84648-1.3764-0.84648-2.233zm-3.2451 6.3245
+
  1.2676-1.2675c-0.59443-0.52674-1.3764-0.84648-2.233-0.84648-1.8603 0-3.3684
+
  1.5081-3.3684 3.3684 0 1.8603 1.5081 3.3684 3.3684 3.3684 1.8603 0
+
  3.3684-1.5081 3.3684-3.3684 0-0.69962-0.2133-1.3495-0.57844-1.888l-1.2291
+
  1.2291c-0.16443 0.16446-0.43102 0.16446-0.59545
+
  0-0.16444-0.16438-0.16444-0.43099 0-0.59545z" />
  {:else if name === "checkmark"}
    <path
      fill-rule="evenodd"
@@ -300,21 +358,31 @@
    19.875 12 19.875Z" />
  {:else if name === "twitter"}
    <path
-
      d="M19.9687 7.54849C19.3697 7.81214 18.7351 7.98617 18.0853
-
    8.06498C18.7694 7.65395 19.2816 7.00936 19.5273 6.25025C18.8933 6.62013
-
    18.1907 6.88937 17.4427 7.03932C16.9492 6.51179 16.2952 6.1619 15.5824
-
    6.04399C14.8696 5.92608 14.1378 6.04675 13.5006 6.38727C12.8635 6.72778
-
    12.3566 7.26908 12.0587 7.92711C11.7608 8.58514 11.6886 9.32307 11.8533
-
    10.0263C9.12667 9.8977 6.71133 8.58814 5.09333 6.61013C4.7992 7.10984
-
    4.64578 7.67979 4.64933 8.25958C4.64933 9.3992 5.22933 10.4009 6.108
-
    10.9893C5.58724 10.9728 5.07798 10.832 4.62267 10.5788V10.6188C4.62237
-
    11.3761 4.88418 12.1103 5.36367 12.6966C5.84316 13.283 6.51081 13.6854
-
    7.25333 13.8357C6.7722 13.9646 6.26828 13.984 5.77867 13.8924C5.98941
-
    14.5442 6.39844 15.1139 6.94868 15.5222C7.49891 15.9304 8.1629 16.1567
-
    8.848 16.1696C7.68769 17.0799 6.25498 17.574 4.78 17.5725C4.52 17.5725
-
    4.26067 17.5571 4 17.5278C5.50381 18.4904 7.25234 19.0013 9.038 19C15.0733
-
    19 18.37 14.0043 18.37 9.67978C18.37 9.53982 18.37 9.39987 18.36
-
    9.25992C19.004 8.79665 19.5595 8.22147 20 7.56182L19.9687 7.54849Z" />
+
      clip-rule="evenodd"
+
      fill-rule="evenodd"
+
      d="m5.5228 5.4809c0.13886 0.00675 0.26343 0.087463 0.32634 0.21144 1.08
+
  2.1284 2.6203 3.5913 4.862 4.2088v-0.47702c0-1.8169 1.4729-3.2898
+
  3.2898-3.2898 1.1961 0 2.2689 0.56288 2.8469 1.5482h2.7652c0.16263 0 0.30793
+
  0.1017 0.36366 0.25453 0.05566 0.15283 0.0098 0.32417-0.11464 0.42881l-2.5734
+
  2.1625c-0.04079 1.6914-0.6455 3.5129-1.7259 4.9881-1.2652 1.7277-3.2027
+
  3.0031-5.6713 3.0031-2.4028
+
  0-3.8426-0.92007-4.6769-1.8739-0.41221-0.47133-0.66961-0.94259-0.82448-1.2974-0.077555-0.17765-0.12983-0.3272-0.16319-0.43441-0.016689-0.05364-0.028687-0.09676-0.036769-0.12772-0.00405-0.01548-0.00711-0.02794-0.0093-0.03716l-0.00265-0.0113-8.515e-4
+
  -0.0038-3.096e-4 -0.0014-1.316e-4 -6.19e-4c-5.42e-5 -2.33e-4 -1.084e-4
+
  -4.65e-4 0.3781-0.08267l-0.37821 0.08221c-0.024863-0.11441 0.00341-0.23393
+
  0.076873-0.32511 0.073475-0.09111 0.18426-0.14413
+
  0.30134-0.14413h2.3072c-0.87844-0.64588-1.7455-1.5204-2.2917-2.612-0.80618-1.6109-0.88662-3.6435
+
  0.60706-5.9904 0.074644-0.11729 0.20646-0.18553 0.34532-0.17877zm-0.42433
+
  9.5552c3.561e-4 7.74e-4 7.044e-4 0.0016 0.00106 0.0024 0.13017 0.29833 0.34822
+
  0.69791 0.69767 1.0974 0.68907 0.78785 1.9174 1.6095 4.0942 1.6095 2.1745 0
+
  3.8969-1.1162 5.0467-2.6864 1.0242-1.3984 1.5785-3.1389 1.5785-4.713 0-0.11431
+
  0.05055-0.22277 0.13802-0.29631l1.8961-1.5933h-1.9347c-0.14847
+
  0-0.28385-0.084955-0.34849-0.21865-0.39749-0.82249-1.2464-1.3295-2.2666-1.3295-1.3894
+
  0-2.5157 1.1263-2.5157 2.5157v0.96759c0 0.21372-0.17332 0.38704-0.38704
+
  0.38704-0.01664
+
  0-0.03298-0.0011-0.04908-0.0031-0.01757-0.0013-0.03522-0.0039-0.05295-0.0077-2.5186-0.54599-4.2771-2.0072-5.5042-4.1128-0.9927
+
  1.8701-0.84393 3.4204-0.22929 4.6486 0.6925 1.3838 2.004 2.4102 3.1211 3.0036
+
  0.15696 0.08337 0.23697 0.26295 0.19401 0.43534-0.042953 0.17246-0.19784
+
  0.29353-0.37557 0.29353z" />
  {:else}
    {unreachable(name)}
  {/if}
modified src/components/TextInput.svelte
@@ -31,7 +31,7 @@
  });

  function handleKeydown(event: KeyboardEvent) {
-
    if (event.key === "Enter") {
+
    if (event.key === "Enter" && valid) {
      dispatch("submit");
    }
  }
modified src/lib/utils.ts
@@ -16,10 +16,12 @@ import { base } from "@app/lib/router";
import { config } from "@app/lib/config";
import { getAddress, getResolver } from "@app/lib/registrar";
import { getAvatar, getSeed, getRegistration } from "@app/lib/registrar";
+
import { getInfo } from "@app/lib/vesting";

export enum AddressType {
  Contract,
  Org,
+
  Vesting,
  EOA,
}

@@ -127,6 +129,11 @@ export function formatAddress(input: string): string {
  );
}

+
// Returns a shortened Ethereum transaction hash
+
export function formatTx(input: string): string {
+
  return input.substring(0, 20) + "…";
+
}
+

export function formatCommit(oid: string): string {
  return oid.substring(0, 7);
}
@@ -288,12 +295,13 @@ export function isFulfilled<T>(
  return input.status === "fulfilled";
}

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

// Format a name.
@@ -349,6 +357,10 @@ export async function identifyAddress(
  const bytes = ethers.utils.arrayify(code);

  if (bytes.length > 0) {
+
    const info = await getInfo(address, wallet);
+
    if (info) {
+
      return AddressType.Vesting;
+
    }
    return AddressType.Contract;
  }
  return AddressType.EOA;
modified src/lib/vesting.ts
@@ -1,12 +1,13 @@
import type { Wallet } from "@app/lib/wallet";
+
import type { TransactionResponse } from "@ethersproject/abstract-provider";

-
import { ethers } from "ethers";
+
import { BigNumber, ethers } from "ethers";
import { writable } from "svelte/store";

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

export interface VestingInfo {
  token: string;
@@ -20,15 +21,24 @@ export interface VestingInfo {
  vestingPeriod: string;
}

-
export const state = writable<
-
  "idle" | "loading" | "withdrawingSign" | "withdrawing" | "withdrawn"
-
>("idle");
+
export type VestingState =
+
  | { type: "idle" }
+
  | { type: "loading" }
+
  | { type: "error"; error: string }
+
  | { type: "withdrawingSign" }
+
  | { type: "withdrawing"; tx: TransactionResponse }
+
  | { type: "withdrawn" };
+

+
export const state = writable<VestingState>({ type: "idle" });

export async function withdrawVested(
  address: string,
  wallet: Wallet,
): Promise<void> {
-
  assert(wallet.signer);
+
  if (!wallet.signer) {
+
    state.set({ type: "error", error: "No signer available" });
+
    return;
+
  }

  const contract = new ethers.Contract(
    address,
@@ -37,50 +47,119 @@ export async function withdrawVested(
  );
  const signer = wallet.signer;

-
  state.set("withdrawingSign");
+
  state.set({ type: "withdrawingSign" });

-
  const tx = await contract.connect(signer).withdrawVested();
+
  try {
+
    const tx: TransactionResponse = await contract
+
      .connect(signer)
+
      .withdrawVested();

-
  state.set("withdrawing");
-
  await tx.wait();
+
    state.set({ type: "withdrawing", tx });
+
    await tx.wait();
+
  } catch (e) {
+
    handleEtherErrorState(e, "Unknown error, check the dev console");
+
    return;
+
  }
  session.state.refreshBalance(wallet);
-
  state.set("withdrawn");
+
  state.set({ type: "withdrawn" });
}

-
export async function getInfo(
-
  address: string,
-
  wallet: Wallet,
-
): Promise<VestingInfo> {
-
  const contract = new ethers.Contract(
+
export const getInfo = cache.cached(
+
  async (address: string, wallet: Wallet): Promise<VestingInfo | undefined> => {
+
    const contract = getVestingContract(address, wallet);
+

+
    let vestingInfo:
+
      | [
+
          string,
+
          string,
+
          BigNumber,
+
          BigNumber,
+
          BigNumber,
+
          string,
+
          string,
+
          string,
+
        ]
+
      | undefined = undefined;
+
    let token: string | undefined = undefined;
+

+
    try {
+
      token = await contract.token();
+
      if (!token) {
+
        return undefined;
+
      }
+

+
      const tokenContract = new ethers.Contract(
+
        token,
+
        ethereumContractAbis.token,
+
        wallet.provider,
+
      );
+

+
      vestingInfo = await Promise.all([
+
        tokenContract.symbol(),
+
        contract.beneficiary(),
+
        contract.withdrawableBalance(),
+
        contract.withdrawn(),
+
        contract.totalVestingAmount(),
+
        contract.vestingStartTime(),
+
        contract.vestingPeriod(),
+
        contract.cliffPeriod(),
+
      ]);
+
    } catch (e) {
+
      console.warn(e);
+
      return undefined;
+
    }
+

+
    const [
+
      symbol,
+
      beneficiary,
+
      withdrawable,
+
      withdrawn,
+
      total,
+
      vestingStartTime,
+
      vestingPeriod,
+
      cliffPeriod,
+
    ] = vestingInfo;
+

+
    return {
+
      token,
+
      symbol,
+
      beneficiary,
+
      withdrawableBalance: utils.formatBalance(withdrawable),
+
      withdrawn: utils.formatBalance(withdrawn),
+
      totalVesting: utils.formatBalance(total),
+
      vestingStartTime,
+
      vestingPeriod,
+
      cliffPeriod,
+
    };
+
  },
+
  address => address,
+
  { max: 1000 },
+
);
+

+
export function parseVestingPeriods(...timestamps: string[]): string {
+
  const sum = timestamps
+
    .map(timestamp => parseInt(timestamp))
+
    .reduce((prev, curr) => prev + curr, 0);
+
  return new Date(sum * 1000).toDateString();
+
}
+

+
export function getVestingContract(address: string, wallet: Wallet) {
+
  return new ethers.Contract(
    address,
    ethereumContractAbis.vesting,
    wallet.provider,
  );
-
  const token = await contract.token();
-
  const beneficiary = await contract.beneficiary();
-
  const withdrawable = await contract.withdrawableBalance();
-
  const withdrawn = await contract.withdrawn();
-
  const total = await contract.totalVestingAmount();
-
  const vestingStartTime = await contract.vestingStartTime();
-
  const vestingPeriod = await contract.vestingPeriod();
-
  const cliffPeriod = await contract.cliffPeriod();
-

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

-
  return {
-
    token,
-
    symbol,
-
    beneficiary,
-
    totalVesting: utils.formatBalance(total),
-
    withdrawableBalance: utils.formatBalance(withdrawable),
-
    withdrawn: utils.formatBalance(withdrawn),
-
    vestingStartTime,
-
    vestingPeriod,
-
    cliffPeriod,
-
  };
+
}
+

+
export function handleEtherErrorState(e: unknown, message: string): void {
+
  const error =
+
    typeof e === "object" &&
+
    e !== null &&
+
    "reason" in e &&
+
    typeof e.reason === "string"
+
      ? e.reason
+
      : message;
+

+
  state.set({ type: "error", error });
+
  console.warn(e);
}
modified src/views/profiles/Profile.svelte
@@ -3,6 +3,8 @@
  import type { Wallet } from "@app/lib/wallet";
  import type { Seed, Stats } from "@app/lib/seed";
  import type { ProjectInfo } from "@app/lib/project";
+
  import type { VestingInfo } from "@app/lib/vesting";
+

  import * as utils from "@app/lib/utils";
  import Address from "@app/components/Address.svelte";
  import Async from "@app/components/Async.svelte";
@@ -18,20 +20,31 @@
  import RadicleId from "@app/components/RadicleId.svelte";
  import SeedAddress from "@app/components/SeedAddress.svelte";
  import SetName from "./SetName.svelte";
+
  import Withdraw from "@app/views/vesting/Withdraw.svelte";
  import { MissingReverseRecord, NotFoundError } from "@app/lib/error";
  import { User, Profile, ProfileType } from "@app/lib/profile";
  import { defaultNodePort } from "@app/lib/seed";
+
  import {
+
    getInfo,
+
    getVestingContract,
+
    handleEtherErrorState,
+
    parseVestingPeriods,
+
  } from "@app/lib/vesting";
+
  import { onMount } from "svelte";
  import { session } from "@app/lib/session";

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

-
  let setNameForm: typeof SvelteComponent | null =
-
    action === "setName" ? SetName : null;
+
  let vestingInfo: VestingInfo | undefined = undefined;
+
  let setNameForm: typeof SvelteComponent | undefined = undefined;
+
  let withdrawVestingModal: typeof SvelteComponent | undefined = undefined;
  const setName = () => {
    setNameForm = SetName;
  };
+
  const withdrawVesting = () => {
+
    withdrawVestingModal = Withdraw;
+
  };

  const getProjectsAndStats = async (
    seed: Seed,
@@ -45,6 +58,49 @@
    return { stats, projects };
  };

+
  // Refresh vestingInfo and close modal if addressOrName changes
+
  $: {
+
    vestingInfo = undefined;
+
    withdrawVestingModal = undefined;
+
    getInfo(addressOrName, wallet)
+
      .then(info => {
+
        vestingInfo = info;
+
      })
+
      .catch(() => {
+
        console.warn("Not able to get vesting contract info");
+
      });
+
  }
+

+
  onMount(async () => {
+
    const addressType = await utils.identifyAddress(addressOrName, wallet);
+
    if (addressType === utils.AddressType.Contract) {
+
      try {
+
        vestingInfo = await getInfo(addressOrName, wallet);
+
      } catch (e) {
+
        handleEtherErrorState(e, "Not able to get vesting contract info");
+
      }
+
    }
+
  });
+

+
  const vestingContract = getVestingContract(addressOrName, wallet);
+

+
  wallet.provider.on("block", async () => {
+
    if (vestingInfo) {
+
      try {
+
        const updatedAmounts = await Promise.all([
+
          vestingContract.withdrawableBalance(),
+
          vestingContract.withdrawn(),
+
        ]);
+
        vestingInfo.withdrawableBalance = utils.formatBalance(
+
          updatedAmounts[0],
+
        );
+
        vestingInfo.withdrawn = utils.formatBalance(updatedAmounts[1]);
+
      } catch (e) {
+
        handleEtherErrorState(e, "Not able to update the balance");
+
      }
+
    }
+
  });
+

  $: isUserAuthorized = (address: string): boolean | null => {
    return $session && utils.isAddressEqual(address, $session.address);
  };
@@ -75,7 +131,7 @@
  }
  .fields {
    display: grid;
-
    grid-template-columns: 5rem 4fr 2fr;
+
    grid-template-columns: max-content 4fr 2fr;
    gap: 1rem 2rem;
    margin-bottom: 1rem;
  }
@@ -116,7 +172,7 @@
      padding: 1.5rem;
    }
    .fields {
-
      grid-template-columns: 5rem auto;
+
      grid-template-columns: max-content auto;
    }
  }
</style>
@@ -169,6 +225,14 @@
              <Icon name="github" />
            </a>
          {/if}
+
          {#if utils.isAddress(profile.address)}
+
            <a
+
              class="url"
+
              title="Lookup address on Etherscan"
+
              href={utils.explorerLink(profile.address, wallet)}>
+
              <Icon name="etherscan" />
+
            </a>
+
          {/if}
        </div>
      </div>
    </header>
@@ -255,6 +319,64 @@
          {/if}
        </div>
      {/if}
+
      {#if vestingInfo}
+
        <div class="txt-highlight">Vesting Beneficiary</div>
+
        <div style:display="flex" style:gap="1rem">
+
          <Address address={vestingInfo.beneficiary} {wallet} resolve compact />
+
        </div>
+
        <div class="layout-desktop" />
+
        <div class="txt-highlight">Allocation</div>
+
        <div>
+
          {vestingInfo.totalVesting}
+
          <span class="txt-bold">{vestingInfo.symbol}</span>
+
        </div>
+
        <div class="layout-desktop" />
+
        <div class="txt-highlight">Withdrawn</div>
+
        <div>
+
          {vestingInfo.withdrawn}
+
          <span class="txt-bold">{vestingInfo.symbol}</span>
+
        </div>
+
        <div class="layout-desktop" />
+
        <div class="txt-highlight">Withdrawable</div>
+
        <div>
+
          {vestingInfo.withdrawableBalance}
+
          <span class="txt-bold">{vestingInfo.symbol}</span>
+
        </div>
+
        <div class="layout-desktop">
+
          {#if isUserAuthorized(vestingInfo.beneficiary) && parseFloat(vestingInfo.withdrawableBalance) > 0}
+
            <Button variant="secondary" size="small" on:click={withdrawVesting}>
+
              Withdraw
+
            </Button>
+
          {/if}
+
        </div>
+
        <div class="txt-highlight">Start Time</div>
+
        <div>
+
          <span>
+
            {parseVestingPeriods(vestingInfo.vestingStartTime)}
+
          </span>
+
        </div>
+
        <div class="layout-desktop" />
+
        <div class="txt-highlight">Cliff Period End</div>
+
        <div>
+
          <span>
+
            {parseVestingPeriods(
+
              vestingInfo.vestingStartTime,
+
              vestingInfo.cliffPeriod,
+
            )}
+
          </span>
+
        </div>
+
        <div class="layout-desktop" />
+
        <div class="txt-highlight">Vesting Period End</div>
+
        <div>
+
          <span>
+
            {parseVestingPeriods(
+
              vestingInfo.vestingStartTime,
+
              vestingInfo.vestingPeriod,
+
            )}
+
          </span>
+
        </div>
+
        <div class="layout-desktop" />
+
      {/if}
    </div>

    {#if profile.seed?.valid}
@@ -269,10 +391,17 @@
  </main>

  <svelte:component
+
    this={withdrawVestingModal}
+
    info={vestingInfo}
+
    contractAddress={addressOrName}
+
    {wallet}
+
    on:close={() => (withdrawVestingModal = undefined)} />
+

+
  <svelte:component
    this={setNameForm}
    entity={new User(profile.address)}
    {wallet}
-
    on:close={() => (setNameForm = null)} />
+
    on:close={() => (setNameForm = undefined)} />
{:catch err}
  {#if err instanceof NotFoundError}
    <NotFound
modified src/views/vesting/Form.svelte
@@ -18,24 +18,23 @@
      : "";

  const loadContract = async () => {
-
    state.set("loading");
+
    state.set({ type: "loading" });
    try {
      const info = await getInfo(contractAddress, wallet);
-
      router.push({
-
        resource: "vesting",
-
        params: {
-
          view: {
-
            resource: "view",
-
            params: { contract: contractAddress, info },
-
          },
-
        },
-
      });
+
      if (info) {
+
        router.push({
+
          resource: "profile",
+
          params: { addressOrName: contractAddress },
+
        });
+
      } else {
+
        validationMessage = "No vesting account found under this address.";
+
      }
    } catch (error) {
      validationMessage =
        "Couldn't load contract, check dev console for details.";
-
      console.error(error);
+
      console.warn(error);
    }
-
    state.set("idle");
+
    state.set({ type: "idle" });
  };
</script>

@@ -75,16 +74,16 @@
      placeholder="Enter vesting contract address"
      {valid}
      {validationMessage}
-
      loading={$state === "loading"}
-
      disabled={$state === "loading"}
+
      loading={$state.type === "loading"}
+
      disabled={$state.type === "loading"}
      on:submit={loadContract}
      bind:value={contractAddress} />

    <Button
      on:click={loadContract}
      variant="primary"
-
      waiting={$state === "loading"}
-
      disabled={!valid || $state === "loading"}>
+
      waiting={$state.type === "loading"}
+
      disabled={!valid || $state.type === "loading"}>
      Load
    </Button>
  </div>
modified src/views/vesting/Routes.svelte
@@ -1,22 +1,16 @@
<script lang="ts">
  import type { Wallet } from "@app/lib/wallet";
  import type { VestingRoute } from "@app/lib/router/definitions";
-
  import type { Session } from "@app/lib/session";

-
  import Form from "./Form.svelte";
-
  import View from "./View.svelte";
+
  import Form from "@app/views/vesting/Form.svelte";
+
  import Profile from "@app/views/profiles/Profile.svelte";

  export let activeRoute: VestingRoute;
  export let wallet: Wallet;
-
  export let session: Session | null;
</script>

{#if activeRoute.params.view.resource === "form"}
  <Form {wallet} />
{:else if activeRoute.params.view.resource === "view"}
-
  <View
-
    {wallet}
-
    {session}
-
    info={activeRoute.params.view.params.info}
-
    contractAddress={activeRoute.params.view.params.contract} />
+
  <Profile {wallet} addressOrName={activeRoute.params.view.params.contract} />
{/if}
deleted src/views/vesting/View.svelte
@@ -1,160 +0,0 @@
-
<script lang="ts">
-
  import type { Session } from "@app/lib/session";
-
  import type { Wallet } from "@app/lib/wallet";
-
  import type { VestingInfo } from "@app/lib/vesting";
-

-
  import * as router from "@app/lib/router";
-
  import * as utils from "@app/lib/utils";
-
  import Address from "@app/components/Address.svelte";
-
  import Button from "@app/components/Button.svelte";
-
  import Modal from "@app/components/Modal.svelte";
-
  import { state, getInfo, withdrawVested } from "@app/lib/vesting";
-
  import { onMount } from "svelte";
-
  import ErrorModal from "@app/components/ErrorModal.svelte";
-
  import Loading from "@app/components/Loading.svelte";
-

-
  export let contractAddress: string;
-
  export let info: VestingInfo | null = null;
-
  export let session: Session | null;
-
  export let wallet: Wallet;
-

-
  let error: Error | undefined = undefined;
-

-
  onMount(async () => {
-
    if (!info) {
-
      state.set("loading");
-
      try {
-
        info = await getInfo(contractAddress, wallet);
-
      } catch (e) {
-
        error = e as Error;
-
      }
-
    }
-
    state.set("idle");
-
  });
-

-
  const parseVestingPeriods = (input: string[]): string => {
-
    const total = input
-
      .map(s => parseInt(s))
-
      .reduce((prev, curr) => prev + curr, 0);
-
    return new Date(total * 1000).toDateString();
-
  };
-
</script>
-

-
<style>
-
  table {
-
    table-layout: fixed;
-
    border-collapse: separate;
-
    border-spacing: 2rem 0;
-
  }
-
  td {
-
    text-align: left;
-
    text-overflow: ellipsis;
-
  }
-
</style>
-

-
<svelte:head>
-
  <title>Radicle &ndash; Vesting</title>
-
</svelte:head>
-

-
{#if error}
-
  <ErrorModal
-
    title="Failed to obtain contract information"
-
    message={error.message}
-
    on:close={() => router.pop()} />
-
{:else if $state === "loading"}
-
  <Loading center />
-
{:else if info}
-
  {@const isBeneficiary =
-
    session && utils.isAddressEqual(info.beneficiary, session.address)}
-
  <Modal>
-
    <span slot="title">
-
      {contractAddress}
-
    </span>
-

-
    <span slot="body">
-
      {#if $state === "withdrawn"}
-
        Tokens successfully withdrawn to {utils.formatAddress(
-
          info.beneficiary,
-
        )}.
-
      {:else}
-
        <table>
-
          <tr>
-
            <td class="txt-highlight">Beneficiary</td>
-
            <td>
-
              <Address {wallet} address={info.beneficiary} compact resolve />
-
            </td>
-
          </tr>
-
          <tr>
-
            <td class="txt-highlight">Allocation</td>
-
            <td>
-
              {info.totalVesting}
-
              <span class="txt-bold">{info.symbol}</span>
-
            </td>
-
          </tr>
-
          <tr>
-
            <td class="txt-highlight">Withdrawn</td>
-
            <td>
-
              {info.withdrawn}
-
              <span class="txt-bold">{info.symbol}</span>
-
            </td>
-
          </tr>
-
          <tr>
-
            <td class="txt-highlight">Withdrawable</td>
-
            <td>
-
              {info.withdrawableBalance}
-
              <span class="txt-bold">{info.symbol}</span>
-
            </td>
-
          </tr>
-
          <tr>
-
            <td class="txt-highlight">Start Time</td>
-
            <td>
-
              <span class="txt-bold">
-
                {parseVestingPeriods([info.vestingStartTime])}
-
              </span>
-
            </td>
-
          </tr>
-
          <tr>
-
            <td class="txt-highlight">Cliff Period End</td>
-
            <td>
-
              <span class="txt-bold">
-
                {parseVestingPeriods([info.vestingStartTime, info.cliffPeriod])}
-
              </span>
-
            </td>
-
          </tr>
-
          <tr>
-
            <td class="txt-highlight">Vesting Period End</td>
-
            <td>
-
              <span class="txt-bold">
-
                {parseVestingPeriods([
-
                  info.vestingStartTime,
-
                  info.vestingPeriod,
-
                ])}
-
              </span>
-
            </td>
-
          </tr>
-
        </table>
-
      {/if}
-
    </span>
-

-
    <span slot="actions">
-
      {#if isBeneficiary}
-
        {#if $state === "withdrawingSign"}
-
          <Button disabled waiting={true} variant="primary">
-
            Waiting for signature…
-
          </Button>
-
        {:else if $state === "withdrawing"}
-
          <Button disabled waiting={true} variant="primary">
-
            Withdrawing…
-
          </Button>
-
        {:else if $state === "idle"}
-
          <Button
-
            on:click={() => withdrawVested(contractAddress, wallet)}
-
            variant="primary">
-
            Withdraw
-
          </Button>
-
        {/if}
-
      {/if}
-
      <Button on:click={() => router.pop()} variant="primary">Back</Button>
-
    </span>
-
  </Modal>
-
{/if}
added src/views/vesting/Withdraw.svelte
@@ -0,0 +1,72 @@
+
<script lang="ts" strictEvents>
+
  import type { VestingInfo } from "@app/lib/vesting";
+
  import type { Wallet } from "@app/lib/wallet";
+

+
  import * as utils from "@app/lib/utils";
+
  import Button from "@app/components/Button.svelte";
+
  import ErrorModal from "@app/components/ErrorModal.svelte";
+
  import Loading from "@app/components/Loading.svelte";
+
  import Modal from "@app/components/Modal.svelte";
+
  import { createEventDispatcher, onMount } from "svelte";
+
  import { state } from "@app/lib/vesting";
+
  import { withdrawVested } from "@app/lib/vesting";
+

+
  export let contractAddress: string;
+
  export let info: VestingInfo;
+
  export let wallet: Wallet;
+

+
  const dispatch = createEventDispatcher<{ close: never }>();
+

+
  onMount(async () => {
+
    await withdrawVested(contractAddress, wallet);
+
  });
+
</script>
+

+
<style>
+
  .actions {
+
    display: flex;
+
    justify-content: center;
+
    flex-direction: row;
+
    gap: 1rem;
+
  }
+
</style>
+

+
{#if $state.type === "error"}
+
  <ErrorModal
+
    floating
+
    title="Withdraw failed"
+
    message={$state.error}
+
    on:close={() => dispatch("close")} />
+
{:else}
+
  <Modal on:close floating>
+
    <span slot="title">
+
      {utils.formatAddress(contractAddress)}
+
    </span>
+

+
    <span slot="subtitle">
+
      {#if $state.type === "withdrawingSign"}
+
        <span class="txt-missing">Waiting for a signature…</span>
+
      {:else if $state.type === "withdrawing"}
+
        <span class="txt-missing">Waiting for confirmation…</span>
+
      {/if}
+
    </span>
+

+
    <span slot="body">
+
      {#if $state.type === "withdrawn"}
+
        <span>
+
          Tokens have been withdrawn to <span class="txt-highlight">
+
            {utils.formatAddress(info.beneficiary)}
+
          </span>
+
        </span>
+
      {:else}
+
        <Loading small center />
+
      {/if}
+
    </span>
+

+
    <span class="actions" slot="actions">
+
      <Button variant="foreground" on:click={() => dispatch("close")}>
+
        Close
+
      </Button>
+
    </span>
+
  </Modal>
+
{/if}