Improve vesting discovery and error messages

When loading a ETH address as vesting contract we show a cryptic error
message, which non tech-savy end users don't understand, this commit
changes it to something more human readable.

We allow users to enter and load a contract that probably isn't a
vesting contract.

The `getInfo` function in the `vesting` folder is very prone to possible
errors, and benefits from some parallelization between all the method
calls.

We allow hereby to enter a contract address in search and once on the
contract profile page we provide a link to the detailed vesting
information if available.

Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Sebastian Martinez
2023-01-10 14:15:08 +01:00
parent 8df7d66419
commit b2c82b06bd
12 changed files with 489 additions and 321 deletions
+1 -1
View File
@@ -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"}
+9 -35
View File
@@ -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>
+1
View File
@@ -16,6 +16,7 @@
line-height: 1.6;
height: var(--button-tiny-height);
display: flex;
white-space: nowrap;
}
.foreground {
color: var(--color-foreground-6);
+116 -48
View File
@@ -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}
+1 -1
View File
@@ -31,7 +31,7 @@
});
function handleKeydown(event: KeyboardEvent) {
if (event.key === "Enter") {
if (event.key === "Enter" && valid) {
dispatch("submit");
}
}
+16 -4
View File
@@ -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;
+120 -41
View File
@@ -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();
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;
return {
token,
symbol,
beneficiary,
totalVesting: utils.formatBalance(total),
withdrawableBalance: utils.formatBalance(withdrawable),
withdrawn: utils.formatBalance(withdrawn),
vestingStartTime,
vestingPeriod,
cliffPeriod,
};
state.set({ type: "error", error });
console.warn(e);
}
+135 -6
View File
@@ -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}
@@ -268,11 +390,18 @@
{/if}
</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
+15 -16
View File
@@ -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>
+3 -9
View File
@@ -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}
-160
View File
@@ -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}
+72
View File
@@ -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}