mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-03 10:40:30 +08:00
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:
+2
-2
@@ -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"}
|
||||
|
||||
@@ -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 – 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>
|
||||
@@ -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 – 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}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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") {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user