diff --git a/package.json b/package.json
index 4417ad5..d0d9559 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,7 @@
"use-element-position": "^1.0.13",
"use-local-storage-state": "^19.2.0",
"viem": "^1.21.1",
- "wagmi": "^1.4.12",
+ "wagmi": "^1.4.13",
"yup": "^1.4.0"
},
"devDependencies": {
diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx
index da93c5e..3f50a4b 100644
--- a/src/AppRoutes.tsx
+++ b/src/AppRoutes.tsx
@@ -3,6 +3,8 @@ import React from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { NetworkLayout } from '@layouts/NetworkLayout';
+import { AssetsPage } from '@pages/AssetsPage/AssetsPage.tsx';
+import { Vesting } from '@pages/AssetsPage/Vesting.tsx';
import { DashboardPage } from '@pages/DashboardPage/DashboardPage.tsx';
import { DelegationsPage } from '@pages/DelegationsPage/DelegationsPage.tsx';
import { AddNewGateway } from '@pages/GatewaysPage/AddNewGateway.tsx';
@@ -24,6 +26,10 @@ export const AppRoutes = () => {
} index />
} path="workers/:peerId" />
+ } path="/assets">
+ } index />
+ } path="vestings/:address" />
+
} path="/workers">
} index />
} path="add" />
diff --git a/src/api/contracts/vesting.abi.ts b/src/api/contracts/vesting.abi.ts
index 6c0c1cc..7c2108e 100644
--- a/src/api/contracts/vesting.abi.ts
+++ b/src/api/contracts/vesting.abi.ts
@@ -1,4 +1,43 @@
export const VESTING_CONTRACT_ABI = [
+ {
+ type: 'function',
+ name: 'depositedIntoProtocol',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'duration',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'end',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
{
type: 'function',
name: 'execute',
@@ -46,4 +85,131 @@ export const VESTING_CONTRACT_ABI = [
],
stateMutability: 'nonpayable',
},
+ {
+ type: 'function',
+ name: 'expectedTotalAmount',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'immediateReleaseBIP',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'releasable',
+ inputs: [
+ {
+ name: 'token',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'releasable',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'release',
+ inputs: [
+ {
+ name: 'token',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'released',
+ inputs: [
+ {
+ name: 'token',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'start',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'vestedAmount',
+ inputs: [
+ {
+ name: 'token',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'timestamp',
+ type: 'uint64',
+ internalType: 'uint64',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
] as const;
diff --git a/src/api/contracts/vesting.ts b/src/api/contracts/vesting.ts
new file mode 100644
index 0000000..31a8254
--- /dev/null
+++ b/src/api/contracts/vesting.ts
@@ -0,0 +1,125 @@
+import { useState } from 'react';
+
+import { chunk } from 'lodash-es';
+import { MulticallResult } from 'viem';
+import { useContractReads, useContractWrite, usePublicClient } from 'wagmi';
+
+import { useSquidNetworkHeightHooks } from '@hooks/useSquidNetworkHeightHooks';
+import { useContracts } from '@network/useContracts';
+
+import { errorMessage, WriteContractRes } from './utils';
+import { VESTING_CONTRACT_ABI } from './vesting.abi';
+
+export function useVestings({ addresses }: { addresses?: `0x${string}`[] }) {
+ const contracts = useContracts();
+ const { currentHeight } = useSquidNetworkHeightHooks();
+
+ const { data, isLoading } = useContractReads({
+ contracts: addresses?.flatMap(address => {
+ const vestingContract = { abi: VESTING_CONTRACT_ABI, address } as const;
+ return [
+ {
+ ...vestingContract,
+ functionName: 'start',
+ },
+ {
+ ...vestingContract,
+ functionName: 'end',
+ },
+ {
+ ...vestingContract,
+ functionName: 'depositedIntoProtocol',
+ },
+ {
+ ...vestingContract,
+ functionName: 'releasable',
+ args: [contracts.SQD],
+ },
+ {
+ ...vestingContract,
+ functionName: 'released',
+ args: [contracts.SQD],
+ },
+ {
+ ...vestingContract,
+ functionName: 'expectedTotalAmount',
+ },
+ {
+ ...vestingContract,
+ functionName: 'immediateReleaseBIP',
+ },
+ ] as const;
+ }),
+ allowFailure: true,
+ enabled: !!addresses,
+ blockNumber: currentHeight ? BigInt(currentHeight) : undefined,
+ });
+
+ return {
+ data: data
+ ? chunk(data, 7).map(ch => ({
+ start: unwrapResult(ch[0])?.toString(),
+ end: unwrapResult(ch[1])?.toString(),
+ deposited: unwrapResult(ch[2])?.toString(),
+ releasable: unwrapResult(ch[3])?.toString(),
+ released: unwrapResult(ch[4])?.toString(),
+ total: unwrapResult(ch[5])?.toString(),
+ initialRelease: Number(unwrapResult(ch[6]) || 0) / 100,
+ }))
+ : undefined,
+ isLoading,
+ };
+}
+
+export function useVesting({ address }: { address?: `0x${string}` }) {
+ const { data, isLoading } = useVestings({ addresses: address ? [address] : undefined });
+
+ return {
+ data: data?.[0],
+ isLoading,
+ };
+}
+
+function unwrapResult(result?: MulticallResult): T | undefined {
+ return result?.status === 'success' ? (result.result as T) : undefined;
+}
+
+export function useVestingRelease({ address }: { address?: `0x${string}` }) {
+ const client = usePublicClient();
+ const { setWaitHeight } = useSquidNetworkHeightHooks();
+ const [isLoading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const { SQD } = useContracts();
+
+ const { writeAsync } = useContractWrite({
+ abi: VESTING_CONTRACT_ABI,
+ functionName: 'release',
+ args: [SQD],
+ address,
+ });
+
+ const release = async (): Promise => {
+ setLoading(true);
+
+ try {
+ const tx = await writeAsync();
+
+ const receipt = await client.waitForTransactionReceipt(tx);
+ setWaitHeight(receipt.blockNumber, []);
+
+ return { success: true };
+ } catch (e) {
+ const failedReason = errorMessage(e);
+ setError(failedReason);
+ return { success: false, failedReason };
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return {
+ release,
+ isLoading,
+ error,
+ };
+}
diff --git a/src/layouts/NetworkLayout/NetworkMenu.tsx b/src/layouts/NetworkLayout/NetworkMenu.tsx
index 3594b3c..111e191 100644
--- a/src/layouts/NetworkLayout/NetworkMenu.tsx
+++ b/src/layouts/NetworkLayout/NetworkMenu.tsx
@@ -142,6 +142,7 @@ export const NetworkMenu = ({ onItemClick }: NetworkMenuProps) => {
{/*/>*/}
+
diff --git a/src/lib/formatters/formatters.ts b/src/lib/formatters/formatters.ts
index ec65042..90eae13 100644
--- a/src/lib/formatters/formatters.ts
+++ b/src/lib/formatters/formatters.ts
@@ -1,4 +1,5 @@
import prettyBytes from 'pretty-bytes';
+import { zeroAddress } from 'viem';
import { getAddress } from 'viem/utils';
export function percentFormatter(value?: number | string) {
@@ -23,7 +24,9 @@ export function bytesFormatter(val?: number | string) {
return prettyBytes(Number(val), { maximumFractionDigits: 0 });
}
-export function addressFormatter(val: string, shortify?: boolean) {
+export function addressFormatter(val?: string, shortify?: boolean) {
+ val = val || zeroAddress;
+
const address = getAddress(val);
return shortify ? `${address.substring(0, 6)}...${address?.slice(-4)}` : address;
}
diff --git a/src/pages/DashboardPage/Assets.tsx b/src/pages/AssetsPage/Assets.tsx
similarity index 100%
rename from src/pages/DashboardPage/Assets.tsx
rename to src/pages/AssetsPage/Assets.tsx
diff --git a/src/pages/AssetsPage/AssetsPage.tsx b/src/pages/AssetsPage/AssetsPage.tsx
new file mode 100644
index 0000000..32d2a6e
--- /dev/null
+++ b/src/pages/AssetsPage/AssetsPage.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+
+import { Box } from '@mui/material';
+import { Outlet } from 'react-router-dom';
+
+import { CenteredPageWrapper } from '@layouts/NetworkLayout';
+
+import { MyAssets } from './Assets';
+import { MyVestings } from './Vestings';
+
+export function AssetsPage() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/DashboardPage/ClaimButton.tsx b/src/pages/AssetsPage/ClaimButton.tsx
similarity index 100%
rename from src/pages/DashboardPage/ClaimButton.tsx
rename to src/pages/AssetsPage/ClaimButton.tsx
diff --git a/src/pages/AssetsPage/ReleaseButton.tsx b/src/pages/AssetsPage/ReleaseButton.tsx
new file mode 100644
index 0000000..ea3d277
--- /dev/null
+++ b/src/pages/AssetsPage/ReleaseButton.tsx
@@ -0,0 +1,98 @@
+import React, { useState } from 'react';
+
+import { addressFormatter } from '@lib/formatters/formatters';
+import { Button } from '@mui/material';
+import { useFormik } from 'formik';
+import * as yup from 'yup';
+
+import { fromSqd } from '@api/contracts/utils';
+import { useVesting, useVestingRelease } from '@api/contracts/vesting';
+import { BlockchainContractError } from '@components/BlockchainContractError';
+import { ContractCallDialog } from '@components/ContractCallDialog';
+import { Form, FormikSelect, FormRow } from '@components/Form';
+import { Loader } from '@components/Loader';
+
+export const claimSchema = yup.object({
+ source: yup.string().label('Source').trim().required('Source is required'),
+});
+
+export function ReleaseButton({
+ vesting,
+ disabled,
+}: {
+ vesting: { address: string };
+ disabled?: boolean;
+}) {
+ const { release, error, isLoading } = useVestingRelease({
+ address: vesting.address as `0x${string}`,
+ });
+ const { data, isLoading: isVestingLoading } = useVesting({
+ address: vesting.address as `0x${string}`,
+ });
+
+ const formik = useFormik({
+ initialValues: {
+ source: vesting.address,
+ amount: 0,
+ },
+ validationSchema: claimSchema,
+ validateOnChange: true,
+ validateOnBlur: true,
+ validateOnMount: true,
+
+ onSubmit: async () => {
+ const { failedReason } = await release();
+
+ if (!failedReason) {
+ handleClose();
+ }
+ },
+ });
+
+ const [open, setOpen] = useState(false);
+ const handleOpen = (e: React.UIEvent) => {
+ e.stopPropagation();
+ setOpen(true);
+ };
+ const handleClose = () => setOpen(false);
+
+ return (
+ <>
+
+ {
+ if (!confirmed) return handleClose();
+
+ formik.handleSubmit();
+ }}
+ loading={isLoading}
+ >
+ {isVestingLoading ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+}
diff --git a/src/pages/AssetsPage/Vesting.tsx b/src/pages/AssetsPage/Vesting.tsx
new file mode 100644
index 0000000..7752663
--- /dev/null
+++ b/src/pages/AssetsPage/Vesting.tsx
@@ -0,0 +1,125 @@
+import React from 'react';
+
+import { addressFormatter, percentFormatter } from '@lib/formatters/formatters';
+import { Divider, Stack, styled } from '@mui/material';
+import { Box } from '@mui/system';
+import { useParams, useSearchParams } from 'react-router-dom';
+
+import { formatSqd, fromSqd } from '@api/contracts/utils';
+import { useVesting } from '@api/contracts/vesting';
+import { Card } from '@components/Card';
+import { CopyToClipboard } from '@components/CopyToClipboard';
+import { Loader } from '@components/Loader';
+import { CenteredPageWrapper, NetworkPageTitle } from '@layouts/NetworkLayout';
+import { useContracts } from '@network/useContracts';
+
+import { ReleaseButton } from './ReleaseButton';
+
+export const DescLabel = styled(Box, {
+ name: 'DescLabel',
+})(({ theme }) => ({
+ flex: 0.5,
+ color: theme.palette.text.secondary,
+ whiteSpace: 'balance',
+ maxWidth: theme.spacing(25),
+ fontSize: '1rem',
+}));
+
+export const DescValue = styled(Box, {
+ name: 'DescValue',
+})(({ theme }) => ({
+ flex: 1,
+ marginLeft: theme.spacing(2),
+ overflowWrap: 'anywhere',
+}));
+
+export const Title = styled(Box)(({ theme }) => ({
+ fontSize: '1.25rem',
+ lineHeight: 1,
+ marginBottom: theme.spacing(3),
+}));
+
+export const VestingAddress = styled(Box, {
+ name: 'VestingAddress',
+})(({ theme }) => ({
+ color: theme.palette.importantLink.main,
+ overflowWrap: 'anywhere',
+}));
+
+export function Vesting({ backPath }: { backPath: string }) {
+ const { address } = useParams<{ address: `0x${string}` }>();
+ const { data, isLoading } = useVesting({ address });
+ const { SQD_TOKEN } = useContracts();
+
+ const [searchParams] = useSearchParams();
+
+ if (isLoading) return ;
+ else if (!data || !address) {
+ return Not found;
+ }
+
+ return (
+
+
+
+
+ }
+ />
+
+ }>
+
+
+
+ Contract
+
+
+
+
+
+
+
+ Total
+ {formatSqd(SQD_TOKEN, data.total, 8)}
+
+
+ Available
+
+ {formatSqd(SQD_TOKEN, fromSqd(data.total).minus(fromSqd(data.released)), 8)}
+
+
+
+ Deposited
+
+ {data.deposited ? formatSqd(SQD_TOKEN, data.deposited, 8) : '-'}
+
+
+
+ Releasable
+ {formatSqd(SQD_TOKEN, data.releasable, 8)}
+
+
+
+
+
+
+ Start
+ {data.start || '-'}
+
+
+ End
+ {data.end || '-'}
+
+
+ Initial release
+ {`${formatSqd(SQD_TOKEN, fromSqd(data.total).mul(data.initialRelease / 100), 8)} (${percentFormatter(data.initialRelease)})`}
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/AssetsPage/Vestings.tsx b/src/pages/AssetsPage/Vestings.tsx
new file mode 100644
index 0000000..7b7696f
--- /dev/null
+++ b/src/pages/AssetsPage/Vestings.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+
+import { addressFormatter } from '@lib/formatters/formatters.ts';
+import { Box, Stack, TableBody, TableCell, TableHead, TableRow } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+
+import { formatSqd, fromSqd } from '@api/contracts/utils';
+import { useVestings } from '@api/contracts/vesting';
+import { useMyAssets } from '@api/subsquid-network-squid';
+import { Avatar } from '@components/Avatar';
+import { Card } from '@components/Card';
+import { CopyToClipboard } from '@components/CopyToClipboard';
+import { Loader } from '@components/Loader';
+import { BorderedTable } from '@components/Table/BorderedTable';
+import { NetworkPageTitle } from '@layouts/NetworkLayout';
+import { useContracts } from '@network/useContracts';
+
+import { ReleaseButton } from './ReleaseButton';
+
+export function MyVestings() {
+ const navigate = useNavigate();
+ const { assets, isLoading } = useMyAssets();
+ const { data, isLoading: isVestingsLoading } = useVestings({
+ addresses: assets?.vestings.map(v => v.address as `0x${string}`),
+ });
+ const { SQD_TOKEN } = useContracts();
+
+ if (isLoading || isVestingsLoading) return ;
+
+ return (
+
+
+ {assets.vestings.length ? (
+
+
+
+ Vesting
+ Total
+ Available
+ Releasable
+
+
+
+
+ {assets.vestings.map((vesting, i) => {
+ const d = data?.[i];
+ return (
+ navigate(`vestings/${vesting.address}`)}
+ className="hoverable"
+ key={vesting.address}
+ >
+
+
+
+
+
+
+ {formatSqd(SQD_TOKEN, d?.total)}
+
+ {formatSqd(SQD_TOKEN, fromSqd(d?.total).minus(fromSqd(d?.released)), 8)}
+
+ {formatSqd(SQD_TOKEN, d?.releasable)}
+
+
+
+
+
+
+ );
+ })}
+
+
+ ) : (
+ No items to show
+ )}
+
+ );
+}
diff --git a/src/pages/DashboardPage/DashboardPage.tsx b/src/pages/DashboardPage/DashboardPage.tsx
index 468d6c6..c15e1f4 100644
--- a/src/pages/DashboardPage/DashboardPage.tsx
+++ b/src/pages/DashboardPage/DashboardPage.tsx
@@ -1,18 +1,14 @@
import React from 'react';
-import { Box } from '@mui/material';
import { Outlet } from 'react-router-dom';
import { CenteredPageWrapper } from '@layouts/NetworkLayout';
-import { MyAssets } from './Assets';
import { Workers } from './Workers';
export function DashboardPage() {
return (
-
-
diff --git a/yarn.lock b/yarn.lock
index ed79b43..0e2731b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3169,7 +3169,7 @@ __metadata:
viem: "npm:^1.21.1"
vite: "npm:^5.2.6"
vite-tsconfig-paths: "npm:^4.3.2"
- wagmi: "npm:^1.4.12"
+ wagmi: "npm:^1.4.13"
yup: "npm:^1.4.0"
languageName: unknown
linkType: soft
@@ -12290,7 +12290,7 @@ __metadata:
languageName: node
linkType: hard
-"wagmi@npm:^1.4.12":
+"wagmi@npm:^1.4.13":
version: 1.4.13
resolution: "wagmi@npm:1.4.13"
dependencies: