Add route support to vesting feature

This allows people to either search for their vesting contract through
the search form or visit directly `vesting/<contract address>`

Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Sebastian Martinez
2022-12-02 10:29:49 +01:00
parent 6c363e9313
commit cdef3b5d5e
8 changed files with 216 additions and 105 deletions
+2 -2
View File
@@ -16,7 +16,7 @@
import Projects from "@app/base/projects/View.svelte";
import Registrations from "@app/base/registrations/Routes.svelte";
import Seeds from "@app/base/seeds/Routes.svelte";
import Vesting from "@app/base/vesting/Index.svelte";
import Vesting from "@app/base/vesting/Routes.svelte";
initialize();
@@ -105,7 +105,7 @@
session={$session}
activeRoute={$activeRouteStore} />
{:else if $activeRouteStore.resource === "vesting"}
<Vesting {wallet} session={$session} />
<Vesting {wallet} session={$session} activeRoute={$activeRouteStore} />
{:else if $activeRouteStore.resource === "projects"}
<Projects {wallet} activeRoute={$activeRouteStore} />
{:else if $activeRouteStore.resource === "profile"}
+91
View File
@@ -0,0 +1,91 @@
<script lang="ts">
import type { Wallet } from "@app/wallet";
import * as utils from "@app/utils";
import * as router from "@app/router";
import Button from "@app/Button.svelte";
import TextInput from "@app/TextInput.svelte";
import { state, getInfo } from "./vesting";
export let wallet: Wallet;
let contractAddress = "";
$: valid = utils.isAddress(contractAddress);
$: validationMessage =
contractAddress !== "" && !valid
? "Please enter a valid Ethereum address."
: "";
const loadContract = async () => {
state.set("loading");
try {
const info = await getInfo(contractAddress, wallet);
router.push({
resource: "vesting",
params: {
view: {
resource: "view",
params: { contract: contractAddress, info },
},
},
});
} catch (error) {
validationMessage =
"Couldn't load contract, check dev console for details.";
console.error(error);
}
state.set("idle");
};
</script>
<style>
main {
display: flex;
flex-direction: column;
gap: 1.5rem;
height: 100%;
justify-content: center;
padding-bottom: 24vh;
padding-top: 5rem;
width: 38rem;
}
.title {
color: var(--color-secondary);
font-size: var(--font-size-medium);
}
.form {
display: flex;
gap: 1rem;
}
</style>
<svelte:head>
<title>Radicle &ndash; Vesting</title>
</svelte:head>
<main>
<div class="title">
Your Radicle <span class="txt-bold">vesting contract</span>
</div>
<div class="form">
<TextInput
autofocus
placeholder="Enter vesting contract address"
{valid}
{validationMessage}
loading={$state === "loading"}
disabled={$state === "loading"}
on:submit={loadContract}
bind:value={contractAddress} />
<Button
on:click={loadContract}
variant="primary"
waiting={$state === "loading"}
disabled={!valid || $state === "loading"}>
Load
</Button>
</div>
</main>
+22
View File
@@ -0,0 +1,22 @@
<script lang="ts">
import type { Wallet } from "@app/wallet";
import type { VestingRoute } from "@app/router/definitions";
import type { Session } from "@app/session";
import Form from "@app/base/vesting/Form.svelte";
import View from "@app/base/vesting/View.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} />
{/if}
@@ -3,77 +3,44 @@
import type { Wallet } from "@app/wallet";
import type { VestingInfo } from "./vesting";
import * as router from "@app/router";
import * as utils from "@app/utils";
import Address from "@app/Address.svelte";
import Button from "@app/Button.svelte";
import Modal from "@app/Modal.svelte";
import TextInput from "@app/TextInput.svelte";
import { state, getInfo, withdrawVested } from "./vesting";
import { onMount } from "svelte";
import ErrorModal from "@app/ErrorModal.svelte";
import Loading from "@app/Loading.svelte";
export let wallet: Wallet;
export let contractAddress: string;
export let info: VestingInfo | null = null;
export let session: Session | null;
export let wallet: Wallet;
let contractAddress = "";
let info: VestingInfo | null = null;
let validationMessage: string | undefined = undefined;
let valid: boolean = false;
let error: Error | undefined = undefined;
async function loadContract(wallet: Wallet) {
if (!valid) {
return;
}
state.set("loading");
try {
info = await getInfo(contractAddress, wallet);
} catch (error) {
validationMessage =
"Couldn't load contract, check dev console for details.";
console.error(error);
onMount(async () => {
if (!info) {
state.set("loading");
try {
info = await getInfo(contractAddress, wallet);
} catch (e) {
error = e as Error;
}
}
state.set("idle");
}
});
$: isBeneficiary =
info && session && utils.isAddressEqual(info.beneficiary, session.address);
function validate(address: string) {
if (address === "") {
return { valid: false };
}
if (!utils.isAddress(address)) {
return {
valid: false,
validationMessage: "Please enter a valid Ethereum address.",
};
}
return { valid: true };
}
$: ({ valid, validationMessage } = validate(contractAddress));
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>
main {
display: flex;
flex-direction: column;
gap: 1.5rem;
height: 100%;
justify-content: center;
padding-bottom: 24vh;
padding-top: 5rem;
width: 38rem;
}
.title {
color: var(--color-secondary);
font-size: var(--font-size-medium);
}
.form {
display: flex;
gap: 1rem;
}
table {
table-layout: fixed;
border-collapse: separate;
@@ -89,7 +56,16 @@
<title>Radicle &ndash; Vesting</title>
</svelte:head>
{#if info}
{#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}
@@ -129,6 +105,33 @@
<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>
@@ -151,42 +154,7 @@
</Button>
{/if}
{/if}
<Button
on:click={() => {
info = null;
state.set("idle");
}}
variant="primary">
Back
</Button>
<Button on:click={() => router.pop()} variant="primary">Back</Button>
</span>
</Modal>
{:else}
<main>
<div class="title">
Your Radicle <span class="txt-bold">vesting contract</span>
</div>
<div class="form">
<TextInput
autofocus
placeholder="Enter vesting contract address"
{valid}
{validationMessage}
loading={$state === "loading"}
disabled={$state === "loading"}
on:submit={() => {
loadContract(wallet);
}}
bind:value={contractAddress} />
<Button
on:click={() => loadContract(wallet)}
variant="primary"
waiting={$state === "loading"}
disabled={!valid || $state === "loading"}>
Load
</Button>
</div>
</main>
{/if}
+12 -3
View File
@@ -15,6 +15,9 @@ export interface VestingInfo {
totalVesting: string;
withdrawableBalance: string;
withdrawn: string;
cliffPeriod: string;
vestingStartTime: string;
vestingPeriod: string;
}
export const state = writable<
@@ -58,6 +61,9 @@ export async function getInfo(
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,
@@ -67,11 +73,14 @@ export async function getInfo(
const symbol = await tokenContract.symbol();
return {
token: token,
symbol: symbol,
beneficiary: beneficiary,
token,
symbol,
beneficiary,
totalVesting: utils.formatBalance(total),
withdrawableBalance: utils.formatBalance(withdrawable),
withdrawn: utils.formatBalance(withdrawn),
vestingStartTime,
vestingPeriod,
cliffPeriod,
};
}
+11 -2
View File
@@ -1,12 +1,14 @@
import type { VestingInfo } from "@app/base/vesting/vesting";
export type Route =
| FaucetRoute
| ProjectRoute
| RegistrationRoute
| VestingRoute
| { resource: "home" }
| { resource: "404"; params: { url: string } }
| { resource: "profile"; params: { addressOrName: string } }
| { resource: "seeds"; params: { host: string } }
| { resource: "vesting" };
| { resource: "seeds"; params: { host: string } };
export interface ProjectsParams {
urn: string;
@@ -29,6 +31,12 @@ export interface ProjectsParams {
seed?: string;
}
export interface VestingParams {
view:
| { resource: "form" }
| { resource: "view"; params: { contract: string; info?: VestingInfo } };
}
export interface FaucetParams {
view:
| { resource: "form" }
@@ -56,6 +64,7 @@ export interface RegistrationParams {
export type ProjectRoute = { resource: "projects"; params: ProjectsParams };
export type FaucetRoute = { resource: "faucet"; params: FaucetParams };
export type VestingRoute = { resource: "vesting"; params: VestingParams };
export type RegistrationRoute = {
resource: "registrations";
params: RegistrationParams;
+15 -3
View File
@@ -189,8 +189,16 @@ function pathToRoute(path: string): Route | null {
}
return { resource: "faucet", params: { view: { resource: "form" } } };
}
case "vesting":
return { resource: "vesting" };
case "vesting": {
const contract = segments.shift();
if (contract) {
return {
resource: "vesting",
params: { view: { resource: "view", params: { contract } } },
};
}
return { resource: "vesting", params: { view: { resource: "form" } } };
}
case "seeds": {
const host = segments.shift();
if (host) {
@@ -276,7 +284,11 @@ export function routeToPath(route: Route) {
return `/faucet/withdraw?amount=${route.params.view.params.amount}`;
}
} else if (route.resource === "vesting") {
return "/vesting";
if (route.params.view.resource === "form") {
return "/vesting";
} else if (route.params.view.resource === "view") {
return `/vesting/${route.params.view.params.contract}`;
}
} else if (route.resource === "seeds") {
return `/seeds/${route.params.host}`;
} else if (route.resource === "projects") {
+2 -2
View File
@@ -9,7 +9,7 @@ describe("routeToPath", () => {
test.each([
{ input: { resource: "home" }, output: "/", description: "Home Route" },
{
input: { resource: "vesting" },
input: { resource: "vesting", params: { view: { resource: "form" } } },
output: "/vesting",
description: "Vesting Route",
},
@@ -124,7 +124,7 @@ describe("pathToRoute", () => {
{ input: "/", output: { resource: "home" }, description: "Home Route" },
{
input: "/vesting",
output: { resource: "vesting" },
output: { resource: "vesting", params: { view: { resource: "form" } } },
description: "Vesting Route",
},
{