From b18458032dd0cb953bcdf25a7d15a1c30fa5b7fe Mon Sep 17 00:00:00 2001 From: Katteu Date: Fri, 5 Jul 2024 13:19:39 +0800 Subject: [PATCH 01/18] feat(web): vesting dashboard prototype --- src/components/dashboard/DashboardMenu.tsx | 16 +- src/components/layout/Header.tsx | 4 + .../vesting/VestingPositionTable.tsx | 180 ++++++++++++++++++ src/components/vesting/VestingStatsTable.tsx | 119 ++++++++++++ .../vesting/pages/VestingDashboardPage.tsx | 101 ++++++++++ src/pages/staking/[[...staking_page]].tsx | 2 +- src/pages/vesting/index.tsx | 9 + 7 files changed, 428 insertions(+), 3 deletions(-) create mode 100644 src/components/vesting/VestingPositionTable.tsx create mode 100644 src/components/vesting/VestingStatsTable.tsx create mode 100644 src/components/vesting/pages/VestingDashboardPage.tsx create mode 100644 src/pages/vesting/index.tsx diff --git a/src/components/dashboard/DashboardMenu.tsx b/src/components/dashboard/DashboardMenu.tsx index 3917e3f..a660edf 100644 --- a/src/components/dashboard/DashboardMenu.tsx +++ b/src/components/dashboard/DashboardMenu.tsx @@ -14,7 +14,6 @@ import { Zoom, Typography } from '@mui/material'; -import HomeIcon from '@mui/icons-material/Home'; import MenuOpenIcon from '@mui/icons-material/MenuOpen'; import { useRouter } from 'next/router'; @@ -46,6 +45,19 @@ const links = { ] } +const linksVesting = { + Other: [ + { + name: 'Overview', + link: '/vesting' + }, + { + name: 'Vesting Positions', + link: '/vesting/manage-vesting' + } + ] +} + const DashboardMenu: FC = ({ children }) => { const theme = useTheme() const desktop = useMediaQuery(theme.breakpoints.up('md')) @@ -78,7 +90,7 @@ const DashboardMenu: FC = ({ children }) => { const drawer = (
- {Object.entries(links).map(([category, linkItems], categoryIndex) => ( + {Object.entries((router.asPath.startsWith('/staking') ? links : linksVesting)).map(([category, linkItems], categoryIndex) => (
{category !== "Other" && {category} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 20cb4ea..7caed26 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -43,6 +43,10 @@ const Header: FC = () => { { name: "Staking", link: "/staking" + }, + { + name: "Vesting", + link: "/vesting" } ]; diff --git a/src/components/vesting/VestingPositionTable.tsx b/src/components/vesting/VestingPositionTable.tsx new file mode 100644 index 0000000..64a6145 --- /dev/null +++ b/src/components/vesting/VestingPositionTable.tsx @@ -0,0 +1,180 @@ +import React, { useState, useEffect, useMemo, useRef } from 'react'; +import { + Avatar, + Box, + Button, + Checkbox, + Divider, + Skeleton, + Table, + TableBody, + TableCell, + TableFooter, + TableHead, + TablePagination, + TableRow, + Typography, + useTheme +} from '@mui/material'; +import dayjs from 'dayjs'; +import DashboardCard from '@components/dashboard/DashboardCard'; + +interface IVestingPositionTableProps { + data: T[]; + isLoading: boolean; + selectedRows?: Set; + setSelectedRows?: React.Dispatch>>; +} + +const rowsPerPageOptions = [5, 10, 15]; + +const VestingPositionTable = >({ + data, + isLoading, + selectedRows, + setSelectedRows, +} : IVestingPositionTableProps) => { + const theme = useTheme(); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(rowsPerPageOptions[0]); + const [clicked,setClicked] = useState(false); + + const handleChangePage = (event: React.MouseEvent | null, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const formatData = (data: T, key: keyof T): string => { + const value = data[key]; + if (typeof value === 'number') { + return value.toLocaleString(); + } else if (value instanceof Date) { + return dayjs(value).format('YYYY/MM/DD'); + } + return String(value); + }; + + const camelCaseToTitle = (camelCase: string) => { + const withSpaces = camelCase.replace(/([A-Z])/g, ' $1').trim(); + return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1); + }; + + const isCheckboxDisabled = (item: T) => item.unlockDate > new Date(); + + const handleSelectRow = (item: T) => { + if (setSelectedRows) { + setSelectedRows((prevSelectedRows) => { + const newSelectedRows = new Set(prevSelectedRows); + if(!isCheckboxDisabled(item)){ + if(newSelectedRows.has(item)) { + newSelectedRows.delete(item); + }else{ + newSelectedRows.add(item); + } + } + setClicked((newSelectedRows.size == 0) ? false : true); + return newSelectedRows; + }); + } + }; + + return ( + <> + + + Your Vesting Positions + + + + + + + + {data.length > 0 ? ( + + + + + + {Object.keys(data[0]).map((column) => ( + + {camelCaseToTitle(String(column))} + + ))} + + + + {data.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((item, index) => ( + + + + + + handleSelectRow(item)} + color="secondary" + disabled={isCheckboxDisabled(item)} + /> + + {Object.keys(item).map((key, colIndex) => ( + + {isLoading ? : formatData(item, key as keyof T)} + + ))} + + ))} + + + + + + +
+ ) : ( + + + No data available + + + )} +
+ + ); +}; + +export default VestingPositionTable; diff --git a/src/components/vesting/VestingStatsTable.tsx b/src/components/vesting/VestingStatsTable.tsx new file mode 100644 index 0000000..66e55e5 --- /dev/null +++ b/src/components/vesting/VestingStatsTable.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import { + Avatar, + Box, + Paper, + Skeleton, + Table, + TableBody, + TableCell, + TableFooter, + TableHead, + TablePagination, + TableRow, + useTheme +} from '@mui/material'; +import DashboardCard from '@components/dashboard/DashboardCard'; + +interface IVestingStatsTableProps { + data: T[]; + isLoading: boolean; +} + +const VestingStatsTable = >({data,isLoading}: IVestingStatsTableProps) => { + const theme = useTheme(); + const [page, setPage] = React.useState(0); + const [rowsPerPage, setRowsPerPage] = React.useState(5); + + const formatData = (data: T, key: keyof T): string => { + const value = data[key]; + if (typeof value === 'number') { + return value.toLocaleString(); + } else if (value instanceof Date) { + return dayjs(value).format('YYYY/MM/DD'); + } + return String(value); + } + + const camelCaseToTitle = (camelCase: string) => { + const withSpaces = camelCase.replace(/([A-Z])/g, ' $1').trim(); + return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1); + } + + const columns: (keyof T)[] = data.length > 0 ? Object.keys(data[0]).filter(key => !key.includes('icon')) as (keyof T)[] : []; + + const handleChangePage = (event: React.MouseEvent | null, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + return ( + + {data.length > 0 ? + + + + + {columns.map((column) => ( + {camelCaseToTitle(String(column))} + ))} + + + + {data.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((item, index) => ( + + + {item.icon && } + + {Object.keys(item).map((key, colIndex) => ( + key !== 'icon' && + + {isLoading ? : formatData(item, key as keyof T)} + + ))} + + ))} + + + + + + +
: + + + No data available + + + } +
+ ); +} + +export default VestingStatsTable; diff --git a/src/components/vesting/pages/VestingDashboardPage.tsx b/src/components/vesting/pages/VestingDashboardPage.tsx new file mode 100644 index 0000000..11ecc4e --- /dev/null +++ b/src/components/vesting/pages/VestingDashboardPage.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react' +import DashboardHeader from '@components/dashboard/DashboardHeader' +import { Box, Container, Typography } from '@mui/material' +import VestingStatsTable from '../VestingStatsTable' +import VestingPositionTable from '../VestingPositionTable' + +const VestingDashboardPage = () => { + const [isLoading, setIsLoading] = useState(false); + const [selectedRows, setSelectedRows] = useState>(new Set()); + + return ( + + + + Vesting Dashboard + + + + + + + + + + + ) +} + +export default VestingDashboardPage; + +const staticStatsData = { + data: [ + { + icon: 'https://i.imgur.com/aA4NG2V.png', + name: 'SundaeSwap', + totalTreasury: "1,000,000 CNCT", + totalClaimed: "890,000 CNCT", + frequency: "1 month", + startDate: new Date(), + cliffDate: new Date(), + }, + { + icon: 'https://i.imgur.com/4KkO0mV.jpg', + name: 'Coinecta', + totalTreasury: "500,000 CNCT", + totalClaimed: "450,000 CNCT", + frequency: "1 month", + startDate: new Date(), + cliffDate: new Date(), + }, + { + icon: 'https://i.imgur.com/TAAEyrV.jpg', + name: 'Crashr', + totalTreasury: "2,500,000 CNCT", + totalClaimed: "1,000,000 CNCT", + frequency: "1 month", + startDate: new Date(), + cliffDate: new Date(), + }, + ] +} + +const futureDate = new Date(); +futureDate.setDate(futureDate.getDate() + 7); + +const staticPositionsData = { + data: [ + { + projectName: 'Coinecta', + tokenName: "CNCT", + total: "217.29", + unlockDate: futureDate, + initial: 216.21, + bonus: 1.08, + interest: 0.5, + }, + { + projectName: 'SundaeSwap', + tokenName: "CNCT", + total: "328.59", + unlockDate: new Date(), + initial: 200.21, + bonus: 1.08, + interest: 0.5, + }, + { + projectName: 'Coinecta', + tokenName: "CNCT", + total: "117.19", + unlockDate: new Date(), + initial: 100.21, + bonus: 1.08, + interest: 0.5, + }, + ] +} \ No newline at end of file diff --git a/src/pages/staking/[[...staking_page]].tsx b/src/pages/staking/[[...staking_page]].tsx index 2bded2e..7f81e08 100644 --- a/src/pages/staking/[[...staking_page]].tsx +++ b/src/pages/staking/[[...staking_page]].tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { NextPage } from 'next'; import { useRouter } from 'next/router'; import ErrorPage from '@components/ErrorPage'; diff --git a/src/pages/vesting/index.tsx b/src/pages/vesting/index.tsx new file mode 100644 index 0000000..ea117c5 --- /dev/null +++ b/src/pages/vesting/index.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { NextPage } from 'next'; +import VestingDashboardPage from '@components/vesting/pages/VestingDashboardPage'; + +const Vesting: NextPage = () => { + return ; +} + +export default Vesting; \ No newline at end of file From a60fab9bc4af0c85f6426e4e1f25df58b5415399 Mon Sep 17 00:00:00 2001 From: Katteu Date: Tue, 9 Jul 2024 20:19:30 +0800 Subject: [PATCH 02/18] fix: vesting dashboard update --- src/components/layout/Header.tsx | 6 +- .../vesting/VestingPositionTable.tsx | 15 ++- src/components/vesting/VestingStatsTable.tsx | 2 +- .../vesting/pages/VestingDashboardPage.tsx | 98 +++++++++++++++---- 4 files changed, 97 insertions(+), 24 deletions(-) diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 7caed26..90f966c 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -58,6 +58,10 @@ const Header: FC = () => { localStorage.setItem('darkToggle', temp); }; + function getBaseUrl(pathname: string) { + return `/${pathname.split('/')[1]}`; + } + const NavigationListItem: React.FC = ({ size, fontWeight, page }) => { return ( @@ -73,7 +77,7 @@ const Header: FC = () => { mt: '0', borderRadius: '10px', height: (fontWeight && fontWeight > 500) || (size && size > 20) ? '3px' : '2px', - background: router.pathname === page.link ? theme.palette.primary.main : '', + background: getBaseUrl(router.pathname) === page.link ? theme.palette.primary.main : '', width: '100%', }, }} diff --git a/src/components/vesting/VestingPositionTable.tsx b/src/components/vesting/VestingPositionTable.tsx index 64a6145..79b9e49 100644 --- a/src/components/vesting/VestingPositionTable.tsx +++ b/src/components/vesting/VestingPositionTable.tsx @@ -63,7 +63,7 @@ const VestingPositionTable = >({ return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1); }; - const isCheckboxDisabled = (item: T) => item.unlockDate > new Date(); + const isCheckboxDisabled = (item: T) => item.nextUnlockDate > new Date(); const handleSelectRow = (item: T) => { if (setSelectedRows) { @@ -132,9 +132,9 @@ const VestingPositionTable = >({ }} > - + - + handleSelectRow(item)} @@ -144,7 +144,14 @@ const VestingPositionTable = >({ {Object.keys(item).map((key, colIndex) => ( - {isLoading ? : formatData(item, key as keyof T)} + {key === 'projectName' ? ( + + + {isLoading ? : formatData(item, key)} + + ) : ( + isLoading ? : formatData(item, key as keyof T) + )} ))} diff --git a/src/components/vesting/VestingStatsTable.tsx b/src/components/vesting/VestingStatsTable.tsx index 66e55e5..ecb6913 100644 --- a/src/components/vesting/VestingStatsTable.tsx +++ b/src/components/vesting/VestingStatsTable.tsx @@ -80,7 +80,7 @@ const VestingStatsTable = >({data,isLoading}: IVes }} > - {item.icon && } + {item.icon && } {Object.keys(item).map((key, colIndex) => ( key !== 'icon' && diff --git a/src/components/vesting/pages/VestingDashboardPage.tsx b/src/components/vesting/pages/VestingDashboardPage.tsx index 11ecc4e..775ce26 100644 --- a/src/components/vesting/pages/VestingDashboardPage.tsx +++ b/src/components/vesting/pages/VestingDashboardPage.tsx @@ -1,10 +1,12 @@ import React, { useState } from 'react' import DashboardHeader from '@components/dashboard/DashboardHeader' -import { Box, Container, Typography } from '@mui/material' +import { Box, Container, Grid, Skeleton, Typography, useTheme } from '@mui/material' import VestingStatsTable from '../VestingStatsTable' import VestingPositionTable from '../VestingPositionTable' +import DashboardCard from '@components/dashboard/DashboardCard' const VestingDashboardPage = () => { + const theme = useTheme(); const [isLoading, setIsLoading] = useState(false); const [selectedRows, setSelectedRows] = useState>(new Set()); @@ -17,6 +19,63 @@ const VestingDashboardPage = () => { + + + + + Total Number of Projects + + + {isLoading ? + + : + + 3 + + } + + + + + + + Total Locked Assets (in USD) + + + {isLoading ? + <> + + + : + <> + + $ 266,638.79 + + + } + + + + + + + Total Locked Assets (in ADA) + + + {isLoading ? + <> + + : + <> + + ₳ 9,950,466.91 + + + } + + + + @@ -36,8 +95,8 @@ export default VestingDashboardPage; const staticStatsData = { data: [ { - icon: 'https://i.imgur.com/aA4NG2V.png', - name: 'SundaeSwap', + icon: 'https://i.imgur.com/4KkO0mV.jpg', + projectName: 'SundaeSwap', totalTreasury: "1,000,000 CNCT", totalClaimed: "890,000 CNCT", frequency: "1 month", @@ -46,7 +105,7 @@ const staticStatsData = { }, { icon: 'https://i.imgur.com/4KkO0mV.jpg', - name: 'Coinecta', + projectName: 'Coinecta', totalTreasury: "500,000 CNCT", totalClaimed: "450,000 CNCT", frequency: "1 month", @@ -54,8 +113,8 @@ const staticStatsData = { cliffDate: new Date(), }, { - icon: 'https://i.imgur.com/TAAEyrV.jpg', - name: 'Crashr', + icon: 'https://i.imgur.com/4KkO0mV.jpg', + projectName: 'Crashr', totalTreasury: "2,500,000 CNCT", totalClaimed: "1,000,000 CNCT", frequency: "1 month", @@ -74,28 +133,31 @@ const staticPositionsData = { projectName: 'Coinecta', tokenName: "CNCT", total: "217.29", - unlockDate: futureDate, - initial: 216.21, - bonus: 1.08, - interest: 0.5, + claimable: "15.29", + frequency: "1 month", + nextUnlockDate: futureDate, + endDate: futureDate, + remainingPeriods: "3 periods" }, { projectName: 'SundaeSwap', tokenName: "CNCT", total: "328.59", - unlockDate: new Date(), - initial: 200.21, - bonus: 1.08, - interest: 0.5, + claimable: "200.29", + frequency: "1 month", + nextUnlockDate: new Date(), + endDate: new Date(), + remainingPeriods: "1 period" }, { projectName: 'Coinecta', tokenName: "CNCT", total: "117.19", - unlockDate: new Date(), - initial: 100.21, - bonus: 1.08, - interest: 0.5, + claimable: "16.79", + frequency: "1 month", + nextUnlockDate: new Date(), + endDate: new Date(), + remainingPeriods: "2 periods" }, ] } \ No newline at end of file From 0529290b4becb8669c633033c7952586f7b22b4b Mon Sep 17 00:00:00 2001 From: Katteu Date: Wed, 10 Jul 2024 01:08:42 +0800 Subject: [PATCH 03/18] fix: modified column headings --- src/components/vesting/pages/VestingDashboardPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/vesting/pages/VestingDashboardPage.tsx b/src/components/vesting/pages/VestingDashboardPage.tsx index 775ce26..8112427 100644 --- a/src/components/vesting/pages/VestingDashboardPage.tsx +++ b/src/components/vesting/pages/VestingDashboardPage.tsx @@ -131,7 +131,7 @@ const staticPositionsData = { data: [ { projectName: 'Coinecta', - tokenName: "CNCT", + token: "CNCT", total: "217.29", claimable: "15.29", frequency: "1 month", @@ -141,7 +141,7 @@ const staticPositionsData = { }, { projectName: 'SundaeSwap', - tokenName: "CNCT", + token: "CNCT", total: "328.59", claimable: "200.29", frequency: "1 month", @@ -151,7 +151,7 @@ const staticPositionsData = { }, { projectName: 'Coinecta', - tokenName: "CNCT", + token: "CNCT", total: "117.19", claimable: "16.79", frequency: "1 month", From 04223155b7e783fb32f9894c25fab763ecbd919b Mon Sep 17 00:00:00 2001 From: Christian Benedict Gantuangco Date: Thu, 19 Sep 2024 06:05:38 +0800 Subject: [PATCH 04/18] feat: fetchClaimEntriesByAddress endpoint initial integration to frontend --- .../vesting/VestingPositionTable.tsx | 107 +++++++++++++---- .../vesting/pages/VestingDashboardPage.tsx | 112 +++++++----------- src/server/routers/_app.ts | 2 + src/server/routers/vesting.ts | 21 ++++ src/server/services/vestingApi.ts | 73 ++++++++++++ 5 files changed, 226 insertions(+), 89 deletions(-) create mode 100644 src/server/routers/vesting.ts create mode 100644 src/server/services/vestingApi.ts diff --git a/src/components/vesting/VestingPositionTable.tsx b/src/components/vesting/VestingPositionTable.tsx index 79b9e49..61d4f8c 100644 --- a/src/components/vesting/VestingPositionTable.tsx +++ b/src/components/vesting/VestingPositionTable.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo, useRef } from 'react'; +import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'; import { Avatar, Box, @@ -18,12 +18,17 @@ import { } from '@mui/material'; import dayjs from 'dayjs'; import DashboardCard from '@components/dashboard/DashboardCard'; +import { trpc } from '@lib/utils/trpc'; +import { ClaimEntriesResponse, ClaimTreasuryDataResponse } from '@server/services/vestingApi'; +import { ClaimEntry } from './pages/VestingDashboardPage'; +import WalletSelectDropdown from '@components/WalletSelectDropdown'; +import { useWallet } from '@meshsdk/react'; interface IVestingPositionTableProps { data: T[]; isLoading: boolean; - selectedRows?: Set; - setSelectedRows?: React.Dispatch>>; + connectedAddress?: string; + walletName?: string; } const rowsPerPageOptions = [5, 10, 15]; @@ -31,13 +36,43 @@ const rowsPerPageOptions = [5, 10, 15]; const VestingPositionTable = >({ data, isLoading, - selectedRows, - setSelectedRows, + connectedAddress, + walletName } : IVestingPositionTableProps) => { const theme = useTheme(); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(rowsPerPageOptions[0]); - const [clicked,setClicked] = useState(false); + const { connected, name } = useWallet(); + const [clicked, setClicked] = useState(false); + const [selectedRows, setSelectedRows] = useState>(new Set()); + const [cborAddresses, setCborAddresses] = useState(undefined); + + const _connectedAddress = useMemo(() => connectedAddress, [connectedAddress]); + const _walletName = useMemo(() => walletName, [walletName]); + + const createClaimTreasuryDataMutation = trpc.vesting.createClaimTreasuryData.useMutation(); + const fetchClaimEntriesByAddressMutation = trpc.vesting.fetchClaimEntriesByAddress.useMutation(); + + useEffect(() => { + const execute = async () => { + if (connected) { + const api = await window.cardano[name.toLowerCase()].enable(); + + const addresses = await api.getUsedAddresses(); + setCborAddresses(addresses); + } + } + execute(); + }, [connected, name]); + + const fetchClaimEntries = useCallback(async () => { + if (cborAddresses === undefined) return; + const _claimEntries: ClaimEntriesResponse[] = await fetchClaimEntriesByAddressMutation.mutateAsync({ + addresses: cborAddresses + }); + + console.log('Claim Entries', _claimEntries); + }, [cborAddresses, fetchClaimEntriesByAddressMutation]); const handleChangePage = (event: React.MouseEvent | null, newPage: number) => { setPage(newPage); @@ -63,18 +98,14 @@ const VestingPositionTable = >({ return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1); }; - const isCheckboxDisabled = (item: T) => item.nextUnlockDate > new Date(); - const handleSelectRow = (item: T) => { if (setSelectedRows) { setSelectedRows((prevSelectedRows) => { const newSelectedRows = new Set(prevSelectedRows); - if(!isCheckboxDisabled(item)){ - if(newSelectedRows.has(item)) { - newSelectedRows.delete(item); - }else{ - newSelectedRows.add(item); - } + if(newSelectedRows.has(item)) { + newSelectedRows.delete(item); + } else { + newSelectedRows.add(item); } setClicked((newSelectedRows.size == 0) ? false : true); return newSelectedRows; @@ -82,6 +113,34 @@ const VestingPositionTable = >({ } }; + const getRawUtxos = useCallback(async () => { + if (_walletName !== undefined) { + const api = await window.cardano[_walletName].enable(); + + const rawUtxos = await api.getUtxos(); + + if (rawUtxos === undefined) return; + return rawUtxos; + } + }, [_walletName]); + + const handleOnRedeemClick = useCallback(async () => { + const rawUtxos = await getRawUtxos(); + + const rootHash: string = '617bb218ba8815fe5bd1ee82dddcc7dacda548837409c5963b74b9dce1e0d14d'; + + if (rawUtxos === undefined) return; + console.log('Raw UTxOs', rawUtxos); + + if (_connectedAddress === undefined) return; + const newTreasuryData: ClaimTreasuryDataResponse = await createClaimTreasuryDataMutation.mutateAsync({ + ownerAddress: _connectedAddress, + rootHash: rootHash + }); + + console.log('New Treasury Data', newTreasuryData); + }, [getRawUtxos, _connectedAddress, createClaimTreasuryDataMutation]); + return ( <> >({ Your Vesting Positions - - + + - + {data.length > 0 ? ( @@ -139,7 +196,7 @@ const VestingPositionTable = >({ checked={selectedRows?.has(item)} onChange={() => handleSelectRow(item)} color="secondary" - disabled={isCheckboxDisabled(item)} + disabled={false} /> {Object.keys(item).map((key, colIndex) => ( @@ -180,6 +237,16 @@ const VestingPositionTable = >({ )} + + + ); }; diff --git a/src/components/vesting/pages/VestingDashboardPage.tsx b/src/components/vesting/pages/VestingDashboardPage.tsx index 8112427..433a676 100644 --- a/src/components/vesting/pages/VestingDashboardPage.tsx +++ b/src/components/vesting/pages/VestingDashboardPage.tsx @@ -1,91 +1,65 @@ -import React, { useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import DashboardHeader from '@components/dashboard/DashboardHeader' import { Box, Container, Grid, Skeleton, Typography, useTheme } from '@mui/material' import VestingStatsTable from '../VestingStatsTable' import VestingPositionTable from '../VestingPositionTable' import DashboardCard from '@components/dashboard/DashboardCard' +import { useWallet } from '@meshsdk/react' +import { useWalletContext } from '@contexts/WalletContext' +import { trpc } from '@lib/utils/trpc' +import { ClaimEntriesResponse } from '@server/services/vestingApi' + +export type ClaimEntry = { + rootHash: string; + claimantPkh: string; + vestingValue: number; + directValue: number; + frequency: "NA"; + nextUnlockDate: "NA"; + endDate: "NA"; + remainingPeriods: "NA"; +} const VestingDashboardPage = () => { const theme = useTheme(); const [isLoading, setIsLoading] = useState(false); - const [selectedRows, setSelectedRows] = useState>(new Set()); + const { wallet, connected, name } = useWallet(); + const { sessionData } = useWalletContext(); + const [connectedAddress, setConnectedAddress] = useState(undefined); + const [walletName, setWalletName] = useState(undefined); + const [claimEntriesData, setClaimEntriesData] = useState(undefined); + + useEffect(() => { + const execute = async () => { + if (connected) { + const api = await window.cardano[name.toLowerCase()].enable(); + + const changeAddress = await wallet.getChangeAddress(); + setConnectedAddress(changeAddress); + setWalletName(name.toLowerCase()); + } + }; + execute(); + }, [connected, connectedAddress, name, wallet]); return ( + - + Vesting Dashboard + - - - - - - Total Number of Projects - - - {isLoading ? - - : - - 3 - - } - - - - - - - Total Locked Assets (in USD) - - - {isLoading ? - <> - - - : - <> - - $ 266,638.79 - - - } - - - - - - - Total Locked Assets (in ADA) - - - {isLoading ? - <> - - : - <> - - ₳ 9,950,466.91 - - - } - - - - - - - - + ) } diff --git a/src/server/routers/_app.ts b/src/server/routers/_app.ts index 195ea56..0eb8603 100644 --- a/src/server/routers/_app.ts +++ b/src/server/routers/_app.ts @@ -14,6 +14,7 @@ import { tokensRouter } from "./tokens"; import { userRouter } from "./user"; import { whitelistRouter } from "./whitelist"; import { xerberusRouter } from "./xerberus"; +import { vestingRouter } from "./vesting"; /** * This is the primary router for the server. @@ -34,6 +35,7 @@ export const appRouter = createTRPCRouter({ tokens: tokensRouter, xerberus: xerberusRouter, sync: syncRouter, + vesting: vestingRouter, evm: evmRouter, email: emailRouter }); diff --git a/src/server/routers/vesting.ts b/src/server/routers/vesting.ts new file mode 100644 index 0000000..60879fa --- /dev/null +++ b/src/server/routers/vesting.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { coinectaVestingApi } from "@server/services/vestingApi"; + +export const vestingRouter = createTRPCRouter({ + createClaimTreasuryData: protectedProcedure + .input(z.object({ + rootHash: z.string(), + ownerAddress: z.string(), + })) + .mutation(async ({ input }) => { + return await coinectaVestingApi.createClaimTreasuryData(input); + }), + fetchClaimEntriesByAddress: protectedProcedure + .input(z.object({ + addresses: z.array(z.string()) + })) + .mutation(async ({ input }) => { + return await coinectaVestingApi.fetchClaimEntriesByAddress(input); + }), +}) \ No newline at end of file diff --git a/src/server/services/vestingApi.ts b/src/server/services/vestingApi.ts new file mode 100644 index 0000000..50bbb6d --- /dev/null +++ b/src/server/services/vestingApi.ts @@ -0,0 +1,73 @@ +import { mapAxiosErrorToTRPCError } from "@server/utils/mapErrors"; +import { TRPCError } from "@trpc/server"; +import axios from "axios"; + +export type ClaimEntriesRequest = { + addresses: string[]; +} + +export type ClaimEntriesResponse = { + rootHash: string; + claimantPkh: string; + vestingValue: number; + directValue: number; +} + +export type ClaimTreasuryDataRequest = { + rootHash: string; + ownerAddress: string; +} + +export type ClaimTreasuryDataResponse = { + updatedRootHash: string; + rawProof: string; + rawClaimEntry: string; +} + +export const coinectaVestingApi = { + async createClaimTreasuryData(request: ClaimTreasuryDataRequest): Promise { + try { + const response = await vestingApi.put(`/api/v1/treasury/claim?rootHash=${request.rootHash}&ownerAddress=${request.ownerAddress}`); + return response.data as ClaimTreasuryDataResponse; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error.response?.data ?? "An unknown error occurred", + }); + } else { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An unknown error occurred", + }); + } + } + }, + async fetchClaimEntriesByAddress(request: ClaimEntriesRequest): Promise { + try { + const response = await vestingApi.post("/api/v1/treasury/claimentries", request.addresses); + return response.data as ClaimEntriesResponse[]; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error.response?.data ?? "An unknown error occurred", + }); + } else { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An unknown error occurred", + }); + } + } + } +} + +export const vestingApi = axios.create({ + baseURL: process.env.COINECTA_SYNC_API, + headers: { + "Content-type": "application/json;charset=utf-8", + }, +}); \ No newline at end of file From 102179322591761a1a19644f6f23dc7f673c425f Mon Sep 17 00:00:00 2001 From: Clark Alesna Date: Fri, 20 Sep 2024 00:11:29 +0800 Subject: [PATCH 05/18] fix: update baseURL for vesting API service --- src/server/services/vestingApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/services/vestingApi.ts b/src/server/services/vestingApi.ts index 50bbb6d..b2b4805 100644 --- a/src/server/services/vestingApi.ts +++ b/src/server/services/vestingApi.ts @@ -66,7 +66,7 @@ export const coinectaVestingApi = { } export const vestingApi = axios.create({ - baseURL: process.env.COINECTA_SYNC_API, + baseURL: process.env.COINECTA_VESTING_API, headers: { "Content-type": "application/json;charset=utf-8", }, From 4a05747f027524d820142cae1bd88232bd6597ac Mon Sep 17 00:00:00 2001 From: Christian Benedict Gantuangco Date: Sat, 21 Sep 2024 01:13:44 +0800 Subject: [PATCH 06/18] feat: display vesting positions using the fetchClaimEntriesByAddresses endpoint --- src/components/WalletSelectDropdown.tsx | 25 ++- .../vesting/VestingPositionTable.tsx | 153 +++++++++++++----- .../vesting/pages/VestingDashboardPage.tsx | 51 ------ src/server/services/vestingApi.ts | 12 +- 4 files changed, 141 insertions(+), 100 deletions(-) diff --git a/src/components/WalletSelectDropdown.tsx b/src/components/WalletSelectDropdown.tsx index bf75c20..256bf0e 100644 --- a/src/components/WalletSelectDropdown.tsx +++ b/src/components/WalletSelectDropdown.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { Select, MenuItem, @@ -19,7 +19,11 @@ import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined'; import { walletDataByName } from '@lib/walletsList'; import { useCardano } from '@lib/utils/cardano'; -const WalletSelectDropdown = () => { +interface IWalletSelectDropdown { + onWalletDropDownUpdate?: (addresses: string[]) => void; +} + +const WalletSelectDropdown: FC = ({ onWalletDropDownUpdate }) => { const theme = useTheme() const router = useRouter() const { isWalletConnected: _isWalletConnected, setSelectedAddresses: _setSelectedAddresses, getSelectedAddresses: _getSelectedAddresses } = useCardano() @@ -40,9 +44,17 @@ const WalletSelectDropdown = () => { const localStorageSelectedAddresses = getSelectedAddresses() if (localStorageSelectedAddresses.length > 0) { setSelectedAddress(localStorageSelectedAddresses) + + if (onWalletDropDownUpdate !== undefined) { + onWalletDropDownUpdate(localStorageSelectedAddresses) + } } else { setSelectedAddress(wallets.map((wallet) => wallet.changeAddress)) setSelectedAddresses(wallets.map((wallet) => wallet.changeAddress)) + + if (onWalletDropDownUpdate !== undefined) { + onWalletDropDownUpdate(wallets.map((wallet) => wallet.changeAddress)) + } } setWalletAddresses(wallets.map((wallet) => wallet.changeAddress)) } @@ -65,6 +77,12 @@ const WalletSelectDropdown = () => { router.push('/user/connected-wallets') }; + const handleOnWalletDropDownClose = useCallback(() => { + if (onWalletDropDownUpdate !== undefined) { + onWalletDropDownUpdate(selectedAddresses); + } + }, [selectedAddresses]) + useEffect(() => { const execute = async () => { if (wallets) { @@ -87,6 +105,7 @@ const WalletSelectDropdown = () => { variant="filled" value={selectedAddresses} onChange={handleChange} + onClose={handleOnWalletDropDownClose} renderValue={() => selectedAddresses.length === 0 ? 'No wallet selected' : 'Select displayed wallets'} MenuProps={{ PaperProps: { @@ -132,7 +151,7 @@ const WalletSelectDropdown = () => { -1} disabled={!isConnectedByWallets[item]} /> - + {!isConnectedByWallets[item] && <> diff --git a/src/components/vesting/VestingPositionTable.tsx b/src/components/vesting/VestingPositionTable.tsx index 61d4f8c..bc865ee 100644 --- a/src/components/vesting/VestingPositionTable.tsx +++ b/src/components/vesting/VestingPositionTable.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'; +import React, { useState, useEffect, useMemo, useRef, useCallback, FC } from 'react'; import { Avatar, Box, @@ -20,32 +20,76 @@ import dayjs from 'dayjs'; import DashboardCard from '@components/dashboard/DashboardCard'; import { trpc } from '@lib/utils/trpc'; import { ClaimEntriesResponse, ClaimTreasuryDataResponse } from '@server/services/vestingApi'; -import { ClaimEntry } from './pages/VestingDashboardPage'; import WalletSelectDropdown from '@components/WalletSelectDropdown'; import { useWallet } from '@meshsdk/react'; -interface IVestingPositionTableProps { - data: T[]; +interface IVestingPositionTableProps { isLoading: boolean; connectedAddress?: string; walletName?: string; } +type ClaimEntry = { + token: string; + total: number | string; + claimable: number | string; + frequency: string; + nextUnlockDate: string; + endDate: string; + remainingPeriods: string; +}; + +type ClaimEntriesResponseWithWalletType = { + claimEntry: ClaimEntriesResponse; + walletType: string; +} + const rowsPerPageOptions = [5, 10, 15]; -const VestingPositionTable = >({ - data, +export const shortenString = (input: string): string => { + if (input.length <= 11) { + return input; + } + + const start = input.slice(0, 7); + const end = input.slice(-4); + + return `${start}...${end}`; +} + +const mapClaimEntriesResponseToClaimEntries = (claimEntries: ClaimEntriesResponse[]): ClaimEntry[] => { + return claimEntries.map(entry => { + const total = entry.vestingValue + ? Object.values(entry.vestingValue).reduce((acc, val) => acc + Object.values(val).reduce((sum, num) => sum + num, 0), 0) + : "N/A"; + + const claimable = entry.directValue + ? Object.values(entry.directValue).reduce((acc, val) => acc + Object.values(val).reduce((sum, num) => sum + num, 0), 0) + : "N/A"; + + return { + token: "CNCT", // Assuming claimantPkh is used as a token here + total: typeof total === 'number' ? total : "N/A", + claimable: typeof claimable === 'number' ? claimable : "N/A", + frequency: "N/A", + nextUnlockDate: "N/A", + endDate: "N/A", + remainingPeriods: "N/A" + }; + }); +}; + +const VestingPositionTable: FC = ({ isLoading, connectedAddress, walletName -} : IVestingPositionTableProps) => { +} : IVestingPositionTableProps) => { const theme = useTheme(); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(rowsPerPageOptions[0]); - const { connected, name } = useWallet(); const [clicked, setClicked] = useState(false); const [selectedRows, setSelectedRows] = useState>(new Set()); - const [cborAddresses, setCborAddresses] = useState(undefined); + const [claimEntries, setClaimEntries] = useState([]); const _connectedAddress = useMemo(() => connectedAddress, [connectedAddress]); const _walletName = useMemo(() => walletName, [walletName]); @@ -53,26 +97,19 @@ const VestingPositionTable = >({ const createClaimTreasuryDataMutation = trpc.vesting.createClaimTreasuryData.useMutation(); const fetchClaimEntriesByAddressMutation = trpc.vesting.fetchClaimEntriesByAddress.useMutation(); - useEffect(() => { - const execute = async () => { - if (connected) { - const api = await window.cardano[name.toLowerCase()].enable(); + const getWallets = trpc.user.getWallets.useQuery() - const addresses = await api.getUsedAddresses(); - setCborAddresses(addresses); - } - } - execute(); - }, [connected, name]); + const wallets = useMemo(() => getWallets.data && getWallets.data.wallets, [getWallets]); - const fetchClaimEntries = useCallback(async () => { - if (cborAddresses === undefined) return; - const _claimEntries: ClaimEntriesResponse[] = await fetchClaimEntriesByAddressMutation.mutateAsync({ - addresses: cborAddresses - }); + const getWalletTypeOfAddress = useCallback((address: string) => { + if (wallets === undefined || wallets === null) return; - console.log('Claim Entries', _claimEntries); - }, [cborAddresses, fetchClaimEntriesByAddressMutation]); + const wallet = wallets.find(wallet => wallet.changeAddress == address); + + if (wallet === undefined) return; + + return wallet.type; + }, [wallets]); const handleChangePage = (event: React.MouseEvent | null, newPage: number) => { setPage(newPage); @@ -83,14 +120,15 @@ const VestingPositionTable = >({ setPage(0); }; - const formatData = (data: T, key: keyof T): string => { + const formatData = (data: ClaimEntry, key: keyof ClaimEntry): string => { const value = data[key]; + if (typeof value === 'number') { return value.toLocaleString(); } else if (value instanceof Date) { return dayjs(value).format('YYYY/MM/DD'); } - return String(value); + return shortenString(String(value)); }; const camelCaseToTitle = (camelCase: string) => { @@ -98,7 +136,7 @@ const VestingPositionTable = >({ return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1); }; - const handleSelectRow = (item: T) => { + const handleSelectRow = (item: ClaimEntry) => { if (setSelectedRows) { setSelectedRows((prevSelectedRows) => { const newSelectedRows = new Set(prevSelectedRows); @@ -141,6 +179,37 @@ const VestingPositionTable = >({ console.log('New Treasury Data', newTreasuryData); }, [getRawUtxos, _connectedAddress, createClaimTreasuryDataMutation]); + const fetchClaimEntries = useCallback(async (addresses: string[]) => { + // const claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[] = []; + + // for (const address of addresses) { + // const _claimEntriesResponses: ClaimEntriesResponse[] = await fetchClaimEntriesByAddressMutation.mutateAsync({ + // addresses: [address] + // }); + + // const walletType = getWalletTypeOfAddress(address); + + // const _claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[] = _claimEntriesResponses.map( + // claimEntriesResponse => ({ + // claimEntry: claimEntriesResponse, + // walletType: walletType !== undefined ? walletType : '' + // }) + // ); + + // claimEntriesResponsesWithWalletType.push(..._claimEntriesResponsesWithWalletType); + // } + + const claimEntriesResponse: ClaimEntriesResponse[] = await fetchClaimEntriesByAddressMutation.mutateAsync({ + addresses + }); + + const mappedClaimEntries = mapClaimEntriesResponseToClaimEntries(claimEntriesResponse); + + setClaimEntries(mappedClaimEntries) + + console.log('Claim Entries', mappedClaimEntries); + }, [fetchClaimEntriesByAddressMutation]); + return ( <> >({ Your Vesting Positions - + - {data.length > 0 ? ( + {claimEntries.length > 0 ? (
>({ top: '71px', zIndex: 2, background: theme.palette.background.paper, + textAlign: 'center' } }}> - {Object.keys(data[0]).map((column) => ( + {Object.keys(claimEntries[0]).map((column) => ( {camelCaseToTitle(String(column))} @@ -181,7 +251,7 @@ const VestingPositionTable = >({ - {data.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((item, index) => ( + {claimEntries.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((item, index) => ( >({ /> {Object.keys(item).map((key, colIndex) => ( - - {key === 'projectName' ? ( - - - {isLoading ? : formatData(item, key)} - - ) : ( - isLoading ? : formatData(item, key as keyof T) - )} + + { + isLoading ? : formatData(item, key as keyof ClaimEntry) + } ))} @@ -220,7 +285,7 @@ const VestingPositionTable = >({ component="td" rowsPerPageOptions={rowsPerPageOptions} colSpan={9} - count={data.length} + count={claimEntries.length} rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} @@ -242,7 +307,7 @@ const VestingPositionTable = >({ sx={{ px: '50px', py: '5px', color: 'white'}} variant="contained" disabled={_connectedAddress === undefined} - onClick={fetchClaimEntries} + onClick={handleOnRedeemClick} > Redeem diff --git a/src/components/vesting/pages/VestingDashboardPage.tsx b/src/components/vesting/pages/VestingDashboardPage.tsx index 433a676..b083085 100644 --- a/src/components/vesting/pages/VestingDashboardPage.tsx +++ b/src/components/vesting/pages/VestingDashboardPage.tsx @@ -9,17 +9,6 @@ import { useWalletContext } from '@contexts/WalletContext' import { trpc } from '@lib/utils/trpc' import { ClaimEntriesResponse } from '@server/services/vestingApi' -export type ClaimEntry = { - rootHash: string; - claimantPkh: string; - vestingValue: number; - directValue: number; - frequency: "NA"; - nextUnlockDate: "NA"; - endDate: "NA"; - remainingPeriods: "NA"; -} - const VestingDashboardPage = () => { const theme = useTheme(); const [isLoading, setIsLoading] = useState(false); @@ -27,7 +16,6 @@ const VestingDashboardPage = () => { const { sessionData } = useWalletContext(); const [connectedAddress, setConnectedAddress] = useState(undefined); const [walletName, setWalletName] = useState(undefined); - const [claimEntriesData, setClaimEntriesData] = useState(undefined); useEffect(() => { const execute = async () => { @@ -53,7 +41,6 @@ const VestingDashboardPage = () => { Date: Sat, 21 Sep 2024 02:53:24 +0800 Subject: [PATCH 07/18] fix: display proper wallet icon to the vesting positions table --- .../vesting/VestingPositionTable.tsx | 95 +++++++++++-------- 1 file changed, 56 insertions(+), 39 deletions(-) diff --git a/src/components/vesting/VestingPositionTable.tsx b/src/components/vesting/VestingPositionTable.tsx index bc865ee..2e3c0d2 100644 --- a/src/components/vesting/VestingPositionTable.tsx +++ b/src/components/vesting/VestingPositionTable.tsx @@ -22,6 +22,7 @@ import { trpc } from '@lib/utils/trpc'; import { ClaimEntriesResponse, ClaimTreasuryDataResponse } from '@server/services/vestingApi'; import WalletSelectDropdown from '@components/WalletSelectDropdown'; import { useWallet } from '@meshsdk/react'; +import { walletDataByName } from '@lib/walletsList'; interface IVestingPositionTableProps { isLoading: boolean; @@ -30,6 +31,7 @@ interface IVestingPositionTableProps { } type ClaimEntry = { + rootHash: string; token: string; total: number | string; claimable: number | string; @@ -37,10 +39,13 @@ type ClaimEntry = { nextUnlockDate: string; endDate: string; remainingPeriods: string; + ownerAddress: string; + walletType: string; }; type ClaimEntriesResponseWithWalletType = { claimEntry: ClaimEntriesResponse; + ownerAddress: string; walletType: string; } @@ -57,24 +62,27 @@ export const shortenString = (input: string): string => { return `${start}...${end}`; } -const mapClaimEntriesResponseToClaimEntries = (claimEntries: ClaimEntriesResponse[]): ClaimEntry[] => { - return claimEntries.map(entry => { - const total = entry.vestingValue - ? Object.values(entry.vestingValue).reduce((acc, val) => acc + Object.values(val).reduce((sum, num) => sum + num, 0), 0) +const mapClaimEntriesResponseToClaimEntries = (claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[]): ClaimEntry[] => { + return claimEntriesResponsesWithWalletType.map(entry => { + const total = entry.claimEntry.vestingValue + ? Object.values(entry.claimEntry.vestingValue).reduce((acc, val) => acc + Object.values(val).reduce((sum, num) => sum + num, 0), 0) : "N/A"; - const claimable = entry.directValue - ? Object.values(entry.directValue).reduce((acc, val) => acc + Object.values(val).reduce((sum, num) => sum + num, 0), 0) + const claimable = entry.claimEntry.directValue + ? Object.values(entry.claimEntry.directValue).reduce((acc, val) => acc + Object.values(val).reduce((sum, num) => sum + num, 0), 0) : "N/A"; return { + rootHash: entry.claimEntry.rootHash, token: "CNCT", // Assuming claimantPkh is used as a token here total: typeof total === 'number' ? total : "N/A", claimable: typeof claimable === 'number' ? claimable : "N/A", frequency: "N/A", nextUnlockDate: "N/A", endDate: "N/A", - remainingPeriods: "N/A" + remainingPeriods: "N/A", + ownerAddress: entry.ownerAddress, + walletType: entry.walletType }; }); }; @@ -101,6 +109,8 @@ const VestingPositionTable: FC = ({ const wallets = useMemo(() => getWallets.data && getWallets.data.wallets, [getWallets]); + const walletByName = useCallback((name: string) => walletDataByName(name), []); + const getWalletTypeOfAddress = useCallback((address: string) => { if (wallets === undefined || wallets === null) return; @@ -180,30 +190,27 @@ const VestingPositionTable: FC = ({ }, [getRawUtxos, _connectedAddress, createClaimTreasuryDataMutation]); const fetchClaimEntries = useCallback(async (addresses: string[]) => { - // const claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[] = []; - - // for (const address of addresses) { - // const _claimEntriesResponses: ClaimEntriesResponse[] = await fetchClaimEntriesByAddressMutation.mutateAsync({ - // addresses: [address] - // }); + const claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[] = []; - // const walletType = getWalletTypeOfAddress(address); + for (const address of addresses) { + const _claimEntriesResponses: ClaimEntriesResponse[] = await fetchClaimEntriesByAddressMutation.mutateAsync({ + addresses: [address] + }); - // const _claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[] = _claimEntriesResponses.map( - // claimEntriesResponse => ({ - // claimEntry: claimEntriesResponse, - // walletType: walletType !== undefined ? walletType : '' - // }) - // ); + const walletType = getWalletTypeOfAddress(address); - // claimEntriesResponsesWithWalletType.push(..._claimEntriesResponsesWithWalletType); - // } + const _claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[] = _claimEntriesResponses.map( + claimEntriesResponse => ({ + claimEntry: claimEntriesResponse, + ownerAddress: address, + walletType: walletType !== undefined ? walletType : '' + }) + ); - const claimEntriesResponse: ClaimEntriesResponse[] = await fetchClaimEntriesByAddressMutation.mutateAsync({ - addresses - }); + claimEntriesResponsesWithWalletType.push(..._claimEntriesResponsesWithWalletType); + } - const mappedClaimEntries = mapClaimEntriesResponseToClaimEntries(claimEntriesResponse); + const mappedClaimEntries = mapClaimEntriesResponseToClaimEntries(claimEntriesResponsesWithWalletType); setClaimEntries(mappedClaimEntries) @@ -243,11 +250,14 @@ const VestingPositionTable: FC = ({ }}> - {Object.keys(claimEntries[0]).map((column) => ( - - {camelCaseToTitle(String(column))} - - ))} + {Object.keys(claimEntries[0]) + .filter(key => key !== 'rootHash' && key !== 'ownerAddress' && key !== 'walletType') + .map((column) => ( + + {camelCaseToTitle(String(column))} + + )) + } @@ -259,7 +269,11 @@ const VestingPositionTable: FC = ({ }} > - + = ({ disabled={false} /> - {Object.keys(item).map((key, colIndex) => ( - - { - isLoading ? : formatData(item, key as keyof ClaimEntry) - } - - ))} + {Object.keys(item) + .filter(key => key !== 'rootHash' && key !== 'ownerAddress' && key !== 'walletType') + .map((key, colIndex) => ( + + { + isLoading ? : formatData(item, key as keyof ClaimEntry) + } + + )) + } ))} From 549e8541c1c423886e23fb6a97a15fc615b622ec Mon Sep 17 00:00:00 2001 From: Christian Benedict Gantuangco Date: Sat, 21 Sep 2024 02:53:37 +0800 Subject: [PATCH 08/18] chore: removed unecessary imports --- .../vesting/pages/VestingDashboardPage.tsx | 41 +------------------ 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/src/components/vesting/pages/VestingDashboardPage.tsx b/src/components/vesting/pages/VestingDashboardPage.tsx index b083085..1182ead 100644 --- a/src/components/vesting/pages/VestingDashboardPage.tsx +++ b/src/components/vesting/pages/VestingDashboardPage.tsx @@ -1,13 +1,8 @@ -import React, { useCallback, useEffect, useState } from 'react' -import DashboardHeader from '@components/dashboard/DashboardHeader' -import { Box, Container, Grid, Skeleton, Typography, useTheme } from '@mui/material' -import VestingStatsTable from '../VestingStatsTable' +import React, { useEffect, useState } from 'react' +import { Box, Container, Typography, useTheme } from '@mui/material' import VestingPositionTable from '../VestingPositionTable' -import DashboardCard from '@components/dashboard/DashboardCard' import { useWallet } from '@meshsdk/react' import { useWalletContext } from '@contexts/WalletContext' -import { trpc } from '@lib/utils/trpc' -import { ClaimEntriesResponse } from '@server/services/vestingApi' const VestingDashboardPage = () => { const theme = useTheme(); @@ -52,35 +47,3 @@ const VestingDashboardPage = () => { } export default VestingDashboardPage; - -const staticStatsData = { - data: [ - { - icon: 'https://i.imgur.com/4KkO0mV.jpg', - projectName: 'SundaeSwap', - totalTreasury: "1,000,000 CNCT", - totalClaimed: "890,000 CNCT", - frequency: "1 month", - startDate: new Date(), - cliffDate: new Date(), - }, - { - icon: 'https://i.imgur.com/4KkO0mV.jpg', - projectName: 'Coinecta', - totalTreasury: "500,000 CNCT", - totalClaimed: "450,000 CNCT", - frequency: "1 month", - startDate: new Date(), - cliffDate: new Date(), - }, - { - icon: 'https://i.imgur.com/4KkO0mV.jpg', - projectName: 'Crashr', - totalTreasury: "2,500,000 CNCT", - totalClaimed: "1,000,000 CNCT", - frequency: "1 month", - startDate: new Date(), - cliffDate: new Date(), - }, - ] -} From 1d59239551b4ba6cc497ce023cee59e0b4f0b039 Mon Sep 17 00:00:00 2001 From: Christian Benedict Gantuangco Date: Sat, 21 Sep 2024 04:27:11 +0800 Subject: [PATCH 09/18] feat: added id property for the new FetchClaimEntriesByAddresses endpoint --- src/components/vesting/VestingPositionTable.tsx | 6 ++++-- src/server/services/vestingApi.ts | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/vesting/VestingPositionTable.tsx b/src/components/vesting/VestingPositionTable.tsx index 2e3c0d2..791e1c6 100644 --- a/src/components/vesting/VestingPositionTable.tsx +++ b/src/components/vesting/VestingPositionTable.tsx @@ -31,6 +31,7 @@ interface IVestingPositionTableProps { } type ClaimEntry = { + id: string; rootHash: string; token: string; total: number | string; @@ -73,6 +74,7 @@ const mapClaimEntriesResponseToClaimEntries = (claimEntriesResponsesWithWalletTy : "N/A"; return { + id: entry.claimEntry.id, rootHash: entry.claimEntry.rootHash, token: "CNCT", // Assuming claimantPkh is used as a token here total: typeof total === 'number' ? total : "N/A", @@ -251,7 +253,7 @@ const VestingPositionTable: FC = ({ {Object.keys(claimEntries[0]) - .filter(key => key !== 'rootHash' && key !== 'ownerAddress' && key !== 'walletType') + .filter(key => key !== 'id' && key !== 'rootHash' && key !== 'ownerAddress' && key !== 'walletType') .map((column) => ( {camelCaseToTitle(String(column))} @@ -284,7 +286,7 @@ const VestingPositionTable: FC = ({ /> {Object.keys(item) - .filter(key => key !== 'rootHash' && key !== 'ownerAddress' && key !== 'walletType') + .filter(key => key !== 'id' && key !== 'rootHash' && key !== 'ownerAddress' && key !== 'walletType') .map((key, colIndex) => ( { diff --git a/src/server/services/vestingApi.ts b/src/server/services/vestingApi.ts index a329645..75d3202 100644 --- a/src/server/services/vestingApi.ts +++ b/src/server/services/vestingApi.ts @@ -7,6 +7,7 @@ export type ClaimEntriesRequest = { } export type ClaimEntriesResponse = { + id: string; rootHash: string; claimantPkh: string; vestingValue?: { From 261bd81c1778c21dce964e0af370a470d40b3c9b Mon Sep 17 00:00:00 2001 From: kenjiebalona Date: Sat, 21 Sep 2024 07:38:15 +0800 Subject: [PATCH 10/18] feat: Implement redeem functionality and improve the UI --- .../vesting/VestingPositionTable.tsx | 535 ++++++++++++------ src/server/routers/vesting.ts | 61 +- src/server/services/vestingApi.ts | 152 ++++- 3 files changed, 541 insertions(+), 207 deletions(-) diff --git a/src/components/vesting/VestingPositionTable.tsx b/src/components/vesting/VestingPositionTable.tsx index 791e1c6..cca7f36 100644 --- a/src/components/vesting/VestingPositionTable.tsx +++ b/src/components/vesting/VestingPositionTable.tsx @@ -1,4 +1,11 @@ -import React, { useState, useEffect, useMemo, useRef, useCallback, FC } from 'react'; +import React, { + useState, + useEffect, + useMemo, + useRef, + useCallback, + FC, +} from "react"; import { Avatar, Box, @@ -14,15 +21,18 @@ import { TablePagination, TableRow, Typography, - useTheme -} from '@mui/material'; -import dayjs from 'dayjs'; -import DashboardCard from '@components/dashboard/DashboardCard'; -import { trpc } from '@lib/utils/trpc'; -import { ClaimEntriesResponse, ClaimTreasuryDataResponse } from '@server/services/vestingApi'; -import WalletSelectDropdown from '@components/WalletSelectDropdown'; -import { useWallet } from '@meshsdk/react'; -import { walletDataByName } from '@lib/walletsList'; + useTheme, +} from "@mui/material"; +import dayjs from "dayjs"; +import DashboardCard from "@components/dashboard/DashboardCard"; +import { trpc } from "@lib/utils/trpc"; +import { + ClaimEntriesResponse, + ClaimTreasuryDataResponse, +} from "@server/services/vestingApi"; +import WalletSelectDropdown from "@components/WalletSelectDropdown"; +import { useWallet } from "@meshsdk/react"; +import { walletDataByName } from "@lib/walletsList"; interface IVestingPositionTableProps { isLoading: boolean; @@ -33,6 +43,7 @@ interface IVestingPositionTableProps { type ClaimEntry = { id: string; rootHash: string; + ownerPkh: string; token: string; total: number | string; claimable: number | string; @@ -48,7 +59,7 @@ type ClaimEntriesResponseWithWalletType = { claimEntry: ClaimEntriesResponse; ownerAddress: string; walletType: string; -} +}; const rowsPerPageOptions = [5, 10, 15]; @@ -61,30 +72,41 @@ export const shortenString = (input: string): string => { const end = input.slice(-4); return `${start}...${end}`; -} +}; -const mapClaimEntriesResponseToClaimEntries = (claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[]): ClaimEntry[] => { - return claimEntriesResponsesWithWalletType.map(entry => { - const total = entry.claimEntry.vestingValue - ? Object.values(entry.claimEntry.vestingValue).reduce((acc, val) => acc + Object.values(val).reduce((sum, num) => sum + num, 0), 0) +const mapClaimEntriesResponseToClaimEntries = ( + claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[], +): ClaimEntry[] => { + return claimEntriesResponsesWithWalletType.map((entry) => { + const total = entry.claimEntry.vestingValue + ? Object.values(entry.claimEntry.vestingValue).reduce( + (acc, val) => + acc + Object.values(val).reduce((sum, num) => sum + num, 0), + 0, + ) : "N/A"; - const claimable = entry.claimEntry.directValue - ? Object.values(entry.claimEntry.directValue).reduce((acc, val) => acc + Object.values(val).reduce((sum, num) => sum + num, 0), 0) + const claimable = entry.claimEntry.directValue + ? Object.values(entry.claimEntry.directValue).reduce( + (acc, val) => + acc + Object.values(val).reduce((sum, num) => sum + num, 0), + 0, + ) : "N/A"; return { id: entry.claimEntry.id, rootHash: entry.claimEntry.rootHash, - token: "CNCT", // Assuming claimantPkh is used as a token here - total: typeof total === 'number' ? total : "N/A", - claimable: typeof claimable === 'number' ? claimable : "N/A", + ownerPkh: entry.claimEntry.claimantPkh, + token: "CNCT", // Assuming claimantPkh is used as a token here + total: typeof total === "number" ? total : "N/A", + claimable: typeof claimable === "number" ? claimable : "N/A", frequency: "N/A", nextUnlockDate: "N/A", endDate: "N/A", remainingPeriods: "N/A", ownerAddress: entry.ownerAddress, - walletType: entry.walletType + walletType: entry.walletType, }; }); }; @@ -92,8 +114,8 @@ const mapClaimEntriesResponseToClaimEntries = (claimEntriesResponsesWithWalletTy const VestingPositionTable: FC = ({ isLoading, connectedAddress, - walletName -} : IVestingPositionTableProps) => { + walletName, +}: IVestingPositionTableProps) => { const theme = useTheme(); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(rowsPerPageOptions[0]); @@ -104,47 +126,70 @@ const VestingPositionTable: FC = ({ const _connectedAddress = useMemo(() => connectedAddress, [connectedAddress]); const _walletName = useMemo(() => walletName, [walletName]); - const createClaimTreasuryDataMutation = trpc.vesting.createClaimTreasuryData.useMutation(); - const fetchClaimEntriesByAddressMutation = trpc.vesting.fetchClaimEntriesByAddress.useMutation(); + const finalizeTransaction = trpc.vesting.finalizeTransaction.useMutation(); + const createClaimTreasuryDataMutation = + trpc.vesting.createClaimTreasuryData.useMutation(); + const fetchClaimEntriesByAddressMutation = + trpc.vesting.fetchClaimEntriesByAddress.useMutation(); + const claimTreasuryMutation = trpc.vesting.claimTreasury.useMutation(); + const submitClaimTreasuryTransaction = + trpc.vesting.submitClaimTreasuryTransaction.useMutation(); - const getWallets = trpc.user.getWallets.useQuery() + const getWallets = trpc.user.getWallets.useQuery(); - const wallets = useMemo(() => getWallets.data && getWallets.data.wallets, [getWallets]); + const wallets = useMemo( + () => getWallets.data && getWallets.data.wallets, + [getWallets], + ); - const walletByName = useCallback((name: string) => walletDataByName(name), []); + const walletByName = useCallback( + (name: string) => walletDataByName(name), + [], + ); - const getWalletTypeOfAddress = useCallback((address: string) => { - if (wallets === undefined || wallets === null) return; + const getWalletTypeOfAddress = useCallback( + (address: string) => { + if (wallets === undefined || wallets === null) return; - const wallet = wallets.find(wallet => wallet.changeAddress == address); + const wallet = wallets.find((wallet) => wallet.changeAddress == address); - if (wallet === undefined) return; + if (wallet === undefined) return; - return wallet.type; - }, [wallets]); + return wallet.type; + }, + [wallets], + ); - const handleChangePage = (event: React.MouseEvent | null, newPage: number) => { + const handleChangePage = ( + event: React.MouseEvent | null, + newPage: number, + ) => { setPage(newPage); }; - const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + const handleChangeRowsPerPage = ( + event: React.ChangeEvent, + ) => { setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); }; - const formatData = (data: ClaimEntry, key: keyof ClaimEntry): string => { + const formatData = ( + data: ClaimEntry, + key: keyof ClaimEntry, + ): string => { const value = data[key]; - if (typeof value === 'number') { + if (typeof value === "number") { return value.toLocaleString(); } else if (value instanceof Date) { - return dayjs(value).format('YYYY/MM/DD'); + return dayjs(value).format("YYYY/MM/DD"); } return shortenString(String(value)); }; const camelCaseToTitle = (camelCase: string) => { - const withSpaces = camelCase.replace(/([A-Z])/g, ' $1').trim(); + const withSpaces = camelCase.replace(/([A-Z])/g, " $1").trim(); return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1); }; @@ -152,12 +197,12 @@ const VestingPositionTable: FC = ({ if (setSelectedRows) { setSelectedRows((prevSelectedRows) => { const newSelectedRows = new Set(prevSelectedRows); - if(newSelectedRows.has(item)) { - newSelectedRows.delete(item); + if (newSelectedRows.has(item)) { + newSelectedRows.delete(item); } else { - newSelectedRows.add(item); + newSelectedRows.add(item); } - setClicked((newSelectedRows.size == 0) ? false : true); + setClicked(newSelectedRows.size == 0 ? false : true); return newSelectedRows; }); } @@ -174,163 +219,295 @@ const VestingPositionTable: FC = ({ } }, [_walletName]); - const handleOnRedeemClick = useCallback(async () => { - const rawUtxos = await getRawUtxos(); - - const rootHash: string = '617bb218ba8815fe5bd1ee82dddcc7dacda548837409c5963b74b9dce1e0d14d'; - - if (rawUtxos === undefined) return; - console.log('Raw UTxOs', rawUtxos); + const getRawCollateralUtxo = useCallback(async () => { + if (_walletName !== undefined) { + const api = await window.cardano[_walletName].enable(); - if (_connectedAddress === undefined) return; - const newTreasuryData: ClaimTreasuryDataResponse = await createClaimTreasuryDataMutation.mutateAsync({ - ownerAddress: _connectedAddress, - rootHash: rootHash - }); + const collateral = await api.experimental.getCollateral(); - console.log('New Treasury Data', newTreasuryData); - }, [getRawUtxos, _connectedAddress, createClaimTreasuryDataMutation]); + if (collateral === undefined) return; + return collateral[0]; + } + }, [_walletName]); - const fetchClaimEntries = useCallback(async (addresses: string[]) => { - const claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[] = []; + const signTx = useCallback( + async (unsignedTx: string) => { + if (_walletName !== undefined) { + const api = await window.cardano[_walletName].enable(); - for (const address of addresses) { - const _claimEntriesResponses: ClaimEntriesResponse[] = await fetchClaimEntriesByAddressMutation.mutateAsync({ - addresses: [address] - }); + const signedTx = await api.signTx(unsignedTx, true); - const walletType = getWalletTypeOfAddress(address); + if (signedTx === undefined) return; + return signedTx; + } + }, + [_walletName], + ); - const _claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[] = _claimEntriesResponses.map( - claimEntriesResponse => ({ - claimEntry: claimEntriesResponse, - ownerAddress: address, - walletType: walletType !== undefined ? walletType : '' - }) + const fetchClaimEntries = useCallback( + async (addresses: string[]) => { + const claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[] = + []; + + for (const address of addresses) { + const _claimEntriesResponses: ClaimEntriesResponse[] = + await fetchClaimEntriesByAddressMutation.mutateAsync({ + addresses: [address], + }); + + const walletType = getWalletTypeOfAddress(address); + + const _claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[] = + _claimEntriesResponses.map((claimEntriesResponse) => ({ + claimEntry: claimEntriesResponse, + ownerAddress: address, + walletType: walletType !== undefined ? walletType : "", + })); + + claimEntriesResponsesWithWalletType.push( + ..._claimEntriesResponsesWithWalletType, + ); + } + + const mappedClaimEntries = mapClaimEntriesResponseToClaimEntries( + claimEntriesResponsesWithWalletType, ); - claimEntriesResponsesWithWalletType.push(..._claimEntriesResponsesWithWalletType); - } + setClaimEntries(mappedClaimEntries); - const mappedClaimEntries = mapClaimEntriesResponseToClaimEntries(claimEntriesResponsesWithWalletType); + console.log("Claim Entries", mappedClaimEntries); + }, + [fetchClaimEntriesByAddressMutation], + ); + + const handleOnRedeemClick = useCallback( + async (claimEntry: ClaimEntry) => { + const rawUtxos = await getRawUtxos(); + const rawCollateralUtxo = await getRawCollateralUtxo(); + if (rawUtxos === undefined || rawCollateralUtxo === undefined) return; + + if (_connectedAddress === undefined) return; + + const rootHash: string = claimEntry.rootHash; + const ownerAddress: string = claimEntry.ownerAddress; - setClaimEntries(mappedClaimEntries) + const { updatedRootHash, rawProof, rawClaimEntry } = + await createClaimTreasuryDataMutation.mutateAsync({ + rootHash, + ownerAddress, + }); - console.log('Claim Entries', mappedClaimEntries); - }, [fetchClaimEntriesByAddressMutation]); + const id: string = claimEntry.id; + + const { unsignedTxRaw, treasuryUtxoRaw } = + await claimTreasuryMutation.mutateAsync({ + id, + ownerAddress, + updatedRootHash, + rawProof, + rawClaimEntry, + rawCollateralUtxo, + rawUtxos, + }); + + const signedTx = await signTx(unsignedTxRaw); + + if (signedTx === undefined) return; + + const { txHash } = await finalizeTransaction.mutateAsync({ + unsignedTxCbor: unsignedTxRaw, + txWitnessCbor: signedTx, + }); + + const ownerPkh = claimEntry.ownerPkh; + + await submitClaimTreasuryTransaction.mutateAsync({ + id, + ownerPkh, + utxoRaw: treasuryUtxoRaw, + txRaw: txHash, + }); + }, + [ + getRawUtxos, + getRawCollateralUtxo, + _connectedAddress, + createClaimTreasuryDataMutation, + ], + ); return ( <> - - - Your Vesting Positions - - - + + Your Vesting Positions + + - + {claimEntries.length > 0 ? ( -
- - - - - {Object.keys(claimEntries[0]) - .filter(key => key !== 'id' && key !== 'rootHash' && key !== 'ownerAddress' && key !== 'walletType') - .map((column) => ( - - {camelCaseToTitle(String(column))} - - )) - } - - - - {claimEntries.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((item, index) => ( - - - - - - handleSelectRow(item)} - color="secondary" - disabled={false} - /> - - {Object.keys(item) - .filter(key => key !== 'id' && key !== 'rootHash' && key !== 'ownerAddress' && key !== 'walletType') - .map((key, colIndex) => ( - - { - isLoading ? : formatData(item, key as keyof ClaimEntry) +
+ + + + {Object.keys(claimEntries[0]) + .filter( + (key) => + key !== "id" && + key !== "ownerPkh" && + key !== "rootHash" && + key !== "ownerAddress" && + key !== "walletType", + ) + .map((column) => ( + + {camelCaseToTitle(String(column))} + + ))} + + + + {claimEntries + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((item, index) => ( + + + - )) - } - - ))} - - - - - - -
+ /> +
+ {Object.keys(item) + .filter( + (key) => + key !== "id" && + key !== "ownerPkh" && + key !== "rootHash" && + key !== "ownerAddress" && + key !== "walletType", + ) + .map((key, colIndex) => ( + + {isLoading ? ( + + ) : ( + formatData(item, key as keyof ClaimEntry) + )} + + ))} + + + + + ))} + + + + + + + ) : ( - - - No data available + + No data available - )} - - - ); }; diff --git a/src/server/routers/vesting.ts b/src/server/routers/vesting.ts index 60879fa..f4f72ed 100644 --- a/src/server/routers/vesting.ts +++ b/src/server/routers/vesting.ts @@ -3,19 +3,60 @@ import { createTRPCRouter, protectedProcedure } from "../trpc"; import { coinectaVestingApi } from "@server/services/vestingApi"; export const vestingRouter = createTRPCRouter({ - createClaimTreasuryData: protectedProcedure - .input(z.object({ - rootHash: z.string(), - ownerAddress: z.string(), - })) + createClaimTreasuryData: protectedProcedure + .input( + z.object({ + rootHash: z.string(), + ownerAddress: z.string(), + }), + ) .mutation(async ({ input }) => { return await coinectaVestingApi.createClaimTreasuryData(input); }), - fetchClaimEntriesByAddress: protectedProcedure - .input(z.object({ - addresses: z.array(z.string()) - })) + fetchClaimEntriesByAddress: protectedProcedure + .input( + z.object({ + addresses: z.array(z.string()), + }), + ) .mutation(async ({ input }) => { return await coinectaVestingApi.fetchClaimEntriesByAddress(input); }), -}) \ No newline at end of file + finalizeTransaction: protectedProcedure + .input( + z.object({ + unsignedTxCbor: z.string(), + txWitnessCbor: z.string(), + }), + ) + .mutation(async ({ input }) => { + return await coinectaVestingApi.finalizeTransaction(input); + }), + claimTreasury: protectedProcedure + .input( + z.object({ + id: z.string(), + ownerAddress: z.string(), + updatedRootHash: z.string(), + rawProof: z.string(), + rawClaimEntry: z.string(), + rawCollateralUtxo: z.string(), + rawUtxos: z.array(z.string()), + }), + ) + .mutation(async ({ input }) => { + return await coinectaVestingApi.claimTreasury(input); + }), + submitClaimTreasuryTransaction: protectedProcedure + .input( + z.object({ + id: z.string(), + ownerPkh: z.string(), + utxoRaw: z.string(), + txRaw: z.string(), + }), + ) + .mutation(async ({ input }) => { + return await coinectaVestingApi.submitClaimTreasuryTransaction(input); + }), +}); diff --git a/src/server/services/vestingApi.ts b/src/server/services/vestingApi.ts index 75d3202..3631528 100644 --- a/src/server/services/vestingApi.ts +++ b/src/server/services/vestingApi.ts @@ -4,39 +4,102 @@ import axios from "axios"; export type ClaimEntriesRequest = { addresses: string[]; -} +}; export type ClaimEntriesResponse = { id: string; rootHash: string; claimantPkh: string; vestingValue?: { - [key: string]: { - [key: string]: number - } + [key: string]: { + [key: string]: number; + }; }; - directValue?: { - [key: string]: { - [key: string]: number - } + directValue?: { + [key: string]: { + [key: string]: number; + }; }; -} +}; export type ClaimTreasuryDataRequest = { rootHash: string; ownerAddress: string; -} +}; export type ClaimTreasuryDataResponse = { updatedRootHash: string; rawProof: string; rawClaimEntry: string; -} +}; + +export type ClaimTreasuryRequest = { + id: string; + ownerAddress: string; + updatedRootHash: string; + rawProof: string; + rawClaimEntry: string; + rawCollateralUtxo: string; + rawUtxos: string[]; +}; + +export type ClaimTreasuryResponse = { + unsignedTxRaw: string; + treasuryUtxoRaw: string; +}; + +export type FinalizeTransactionRequest = { + unsignedTxCbor: string; + txWitnessCbor: string; +}; + +export type FinalizeTransactionResponse = { + txHash: string; +}; + +export type SubmitClaimTreasuryTransactionRequest = { + id: string; + ownerPkh: string; + utxoRaw: string; + txRaw: string; +}; + +export type SubmitClaimTreasuryTransactionResponse = { + txHash: string; +}; export const coinectaVestingApi = { - async createClaimTreasuryData(request: ClaimTreasuryDataRequest): Promise { + async finalizeTransaction( + request: FinalizeTransactionRequest, + ): Promise { try { - const response = await vestingApi.put(`/api/v1/treasury/claim?rootHash=${request.rootHash}&ownerAddress=${request.ownerAddress}`); + const response = await vestingApi.post( + "/api/v1/transaction/finalize", + request, + ); + return { txHash: response.data } as FinalizeTransactionResponse; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error.response?.data ?? "An unknown error occurred", + }); + } else { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An unknown error occurred", + }); + } + } + }, + async createClaimTreasuryData( + request: ClaimTreasuryDataRequest, + ): Promise { + try { + const response = await vestingApi.put( + `/api/v1/treasury/claim?rootHash=${request.rootHash}&ownerAddress=${request.ownerAddress}`, + ); return response.data as ClaimTreasuryDataResponse; } catch (error) { if (axios.isAxiosError(error)) { @@ -53,9 +116,14 @@ export const coinectaVestingApi = { } } }, - async fetchClaimEntriesByAddress(request: ClaimEntriesRequest): Promise { + async fetchClaimEntriesByAddress( + request: ClaimEntriesRequest, + ): Promise { try { - const response = await vestingApi.post("/api/v1/treasury/claimentries", request.addresses); + const response = await vestingApi.post( + "/api/v1/treasury/claimentries", + request.addresses, + ); return response.data as ClaimEntriesResponse[]; } catch (error) { if (axios.isAxiosError(error)) { @@ -71,12 +139,60 @@ export const coinectaVestingApi = { }); } } - } -} + }, + async claimTreasury( + request: ClaimTreasuryRequest, + ): Promise { + try { + const response = await vestingApi.post( + "/api/v1/transaction/treasury/claim", + request, + ); + return response.data as ClaimTreasuryResponse; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error.response?.data ?? "An unknown error occurred", + }); + } else { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An unknown error occurred", + }); + } + } + }, + async submitClaimTreasuryTransaction( + request: SubmitClaimTreasuryTransactionRequest, + ): Promise { + try { + const response = await vestingApi.post( + "/api/v1/transaction/treasury/claim/submit", + request, + ); + return response.data as SubmitClaimTreasuryTransactionResponse; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error.response?.data ?? "An unknown error occurred", + }); + } else { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An unknown error occurred", + }); + } + } + }, +}; export const vestingApi = axios.create({ baseURL: process.env.COINECTA_VESTING_API, headers: { "Content-type": "application/json;charset=utf-8", }, -}); \ No newline at end of file +}); From 793a165131f899c7ae851e65f251bc171ebc8c69 Mon Sep 17 00:00:00 2001 From: kenjiebalona Date: Thu, 26 Sep 2024 02:18:26 +0800 Subject: [PATCH 11/18] feat: add redeem confirmation dialog --- src/components/vesting/VestingConfirm.tsx | 233 ++++++++++++++++++ .../vesting/VestingPositionTable.tsx | 147 ++++------- src/server/services/vestingApi.ts | 2 +- 3 files changed, 281 insertions(+), 101 deletions(-) create mode 100644 src/components/vesting/VestingConfirm.tsx diff --git a/src/components/vesting/VestingConfirm.tsx b/src/components/vesting/VestingConfirm.tsx new file mode 100644 index 0000000..3a7b49c --- /dev/null +++ b/src/components/vesting/VestingConfirm.tsx @@ -0,0 +1,233 @@ +import DataSpread from "@components/DataSpread"; +import { useAlert } from "@contexts/AlertContext"; +import { trpc } from "@lib/utils/trpc"; +import { useWallet } from "@meshsdk/react"; +import CloseIcon from "@mui/icons-material/Close"; +import { + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + useMediaQuery, + useTheme, +} from "@mui/material"; +import React, { FC, useState, useCallback } from "react"; + +type ClaimEntry = { + id: string; + rootHash: string; + ownerPkh: string; + token: string; + total: number | string; + claimable: number | string; + frequency: string; + nextUnlockDate: string; + endDate: string; + remainingPeriods: string; + ownerAddress: string; + walletType: string; +}; + +interface IVestingConfirmProps { + open: boolean; + setOpen: React.Dispatch>; + claimEntry: ClaimEntry | null; + // removeClaimEntry: (claimEntry: ClaimEntry) => void; +} + +const VestingConfirm: FC = ({ + open, + setOpen, + claimEntry, + // removeClaimEntry +}) => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + const [isSigning, setIsSigning] = useState(false); + const { addAlert } = useAlert(); + const { connected } = useWallet(); + + const finalizeTransaction = trpc.vesting.finalizeTransaction.useMutation(); + const createClaimTreasuryDataMutation = + trpc.vesting.createClaimTreasuryData.useMutation(); + const claimTreasuryMutation = trpc.vesting.claimTreasury.useMutation(); + const submitClaimTreasuryTransaction = + trpc.vesting.submitClaimTreasuryTransaction.useMutation(); + + const getRawUtxos = useCallback(async (walletType: string) => { + const api = await window.cardano[walletType].enable(); + + const rawUtxos = await api.getUtxos(); + + if (rawUtxos === undefined) return; + return rawUtxos; + }, []); + + const getRawCollateralUtxo = useCallback(async (walletType: string) => { + const api = await window.cardano[walletType].enable(); + + const collateral = await api.experimental.getCollateral(); + + if (collateral === undefined) return; + return collateral[0]; + }, []); + + const signTx = useCallback(async (walletType: string, unsignedTx: string) => { + const api = await window.cardano[walletType].enable(); + + const signedTx = await api.signTx(unsignedTx, true); + + if (signedTx === undefined) return; + return signedTx; + }, []); + + const handleClose = () => setOpen(false); + + const handleSubmit = useCallback(async () => { + setIsSigning(true); + try { + if (claimEntry === null) return; + const walletType = claimEntry.walletType; + + const rawUtxos = await getRawUtxos(walletType); + const rawCollateralUtxo = await getRawCollateralUtxo(walletType); + if (rawUtxos === undefined || rawCollateralUtxo === undefined) return; + + const rootHash: string = claimEntry.rootHash; + const ownerAddress: string = claimEntry.ownerAddress; + + const { updatedRootHash, rawProof, rawClaimEntry } = + await createClaimTreasuryDataMutation.mutateAsync({ + rootHash, + ownerAddress, + }); + + const id: string = claimEntry.id; + + const { unsignedTxRaw, treasuryUtxoRaw } = + await claimTreasuryMutation.mutateAsync({ + id, + ownerAddress, + updatedRootHash, + rawProof, + rawClaimEntry, + rawCollateralUtxo, + rawUtxos, + }); + + const signedTx = await signTx(walletType, unsignedTxRaw); + + if (signedTx === undefined) return; + + const { txHash } = await finalizeTransaction.mutateAsync({ + unsignedTxCbor: unsignedTxRaw, + txWitnessCbor: signedTx, + }); + + const ownerPkh = claimEntry.ownerPkh; + + const response = await submitClaimTreasuryTransaction.mutateAsync({ + id, + ownerPkh, + utxoRaw: treasuryUtxoRaw, + txRaw: txHash, + }); + console.log(response); + setOpen(false); + addAlert("success", "Redeemed Treasury"); + // removeClaimEntry(claimEntry); + } catch (ex: any) { + addAlert("error", "Error redeeming treasury"); + console.error("Error adding stake", ex); + } + setIsSigning(false); + }, [ + getRawUtxos, + getRawCollateralUtxo, + createClaimTreasuryDataMutation, + signTx, + finalizeTransaction, + claimTreasuryMutation, + submitClaimTreasuryTransaction, + claimEntry, + addAlert, + setOpen, + // removeClaimEntry + ]); + + return ( + <> + + + Redeem Treasury + + theme.palette.grey[500], + }} + > + + + + + + + + + + + + + ); +}; + +export default VestingConfirm; diff --git a/src/components/vesting/VestingPositionTable.tsx b/src/components/vesting/VestingPositionTable.tsx index cca7f36..7ad8839 100644 --- a/src/components/vesting/VestingPositionTable.tsx +++ b/src/components/vesting/VestingPositionTable.tsx @@ -33,6 +33,7 @@ import { import WalletSelectDropdown from "@components/WalletSelectDropdown"; import { useWallet } from "@meshsdk/react"; import { walletDataByName } from "@lib/walletsList"; +import VestingConfirm from "./VestingConfirm"; interface IVestingPositionTableProps { isLoading: boolean; @@ -61,6 +62,22 @@ type ClaimEntriesResponseWithWalletType = { walletType: string; }; +function convertLovelaceToAda(lovelace: number | string | undefined): string { + if (!lovelace) { + return "0.000000"; + } + + const lovelaceValue = + typeof lovelace === "string" ? parseFloat(lovelace) : lovelace; + + if (isNaN(lovelaceValue)) { + return "0.000000"; + } + + const ada = lovelaceValue / 1_000_000; + return ada.toFixed(6); +} + const rowsPerPageOptions = [5, 10, 15]; export const shortenString = (input: string): string => { @@ -122,18 +139,14 @@ const VestingPositionTable: FC = ({ const [clicked, setClicked] = useState(false); const [selectedRows, setSelectedRows] = useState>(new Set()); const [claimEntries, setClaimEntries] = useState([]); + const [claimEntry, setClaimEntry] = useState(null); + const [openConfirmationDialog, setOpenConfirmationDialog] = useState(false); const _connectedAddress = useMemo(() => connectedAddress, [connectedAddress]); const _walletName = useMemo(() => walletName, [walletName]); - const finalizeTransaction = trpc.vesting.finalizeTransaction.useMutation(); - const createClaimTreasuryDataMutation = - trpc.vesting.createClaimTreasuryData.useMutation(); const fetchClaimEntriesByAddressMutation = trpc.vesting.fetchClaimEntriesByAddress.useMutation(); - const claimTreasuryMutation = trpc.vesting.claimTreasury.useMutation(); - const submitClaimTreasuryTransaction = - trpc.vesting.submitClaimTreasuryTransaction.useMutation(); const getWallets = trpc.user.getWallets.useQuery(); @@ -208,47 +221,11 @@ const VestingPositionTable: FC = ({ } }; - const getRawUtxos = useCallback(async () => { - if (_walletName !== undefined) { - const api = await window.cardano[_walletName].enable(); - - const rawUtxos = await api.getUtxos(); - - if (rawUtxos === undefined) return; - return rawUtxos; - } - }, [_walletName]); - - const getRawCollateralUtxo = useCallback(async () => { - if (_walletName !== undefined) { - const api = await window.cardano[_walletName].enable(); - - const collateral = await api.experimental.getCollateral(); - - if (collateral === undefined) return; - return collateral[0]; - } - }, [_walletName]); - - const signTx = useCallback( - async (unsignedTx: string) => { - if (_walletName !== undefined) { - const api = await window.cardano[_walletName].enable(); - - const signedTx = await api.signTx(unsignedTx, true); - - if (signedTx === undefined) return; - return signedTx; - } - }, - [_walletName], - ); - const fetchClaimEntries = useCallback( async (addresses: string[]) => { const claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[] = []; - + console.log(addresses); for (const address of addresses) { const _claimEntriesResponses: ClaimEntriesResponse[] = await fetchClaimEntriesByAddressMutation.mutateAsync({ @@ -256,6 +233,7 @@ const VestingPositionTable: FC = ({ }); const walletType = getWalletTypeOfAddress(address); + console.log(walletType); const _claimEntriesResponsesWithWalletType: ClaimEntriesResponseWithWalletType[] = _claimEntriesResponses.map((claimEntriesResponse) => ({ @@ -273,69 +251,32 @@ const VestingPositionTable: FC = ({ claimEntriesResponsesWithWalletType, ); - setClaimEntries(mappedClaimEntries); - - console.log("Claim Entries", mappedClaimEntries); - }, - [fetchClaimEntriesByAddressMutation], - ); - - const handleOnRedeemClick = useCallback( - async (claimEntry: ClaimEntry) => { - const rawUtxos = await getRawUtxos(); - const rawCollateralUtxo = await getRawCollateralUtxo(); - if (rawUtxos === undefined || rawCollateralUtxo === undefined) return; - - if (_connectedAddress === undefined) return; - - const rootHash: string = claimEntry.rootHash; - const ownerAddress: string = claimEntry.ownerAddress; - - const { updatedRootHash, rawProof, rawClaimEntry } = - await createClaimTreasuryDataMutation.mutateAsync({ - rootHash, - ownerAddress, - }); - - const id: string = claimEntry.id; - - const { unsignedTxRaw, treasuryUtxoRaw } = - await claimTreasuryMutation.mutateAsync({ - id, - ownerAddress, - updatedRootHash, - rawProof, - rawClaimEntry, - rawCollateralUtxo, - rawUtxos, - }); - - const signedTx = await signTx(unsignedTxRaw); - - if (signedTx === undefined) return; - - const { txHash } = await finalizeTransaction.mutateAsync({ - unsignedTxCbor: unsignedTxRaw, - txWitnessCbor: signedTx, + const convertedClaimEntries = mappedClaimEntries.map((item) => { + return { + ...item, + claimable: convertLovelaceToAda(item.claimable), + }; }); - const ownerPkh = claimEntry.ownerPkh; + setClaimEntries(convertedClaimEntries); - await submitClaimTreasuryTransaction.mutateAsync({ - id, - ownerPkh, - utxoRaw: treasuryUtxoRaw, - txRaw: txHash, - }); + console.log("Claim Entries", convertedClaimEntries); }, - [ - getRawUtxos, - getRawCollateralUtxo, - _connectedAddress, - createClaimTreasuryDataMutation, - ], + [fetchClaimEntriesByAddressMutation, getWalletTypeOfAddress], ); + const handleOnRedeemClick = (claimEntry: ClaimEntry) => { + setClaimEntry(claimEntry); + setOpenConfirmationDialog(true); + console.log(claimEntry); + }; + + const removeClaimEntry = (claimEntry: ClaimEntry) => { + setClaimEntries((prevEntries) => + prevEntries.filter((entry) => entry.id !== claimEntry.id), + ); + }; + return ( <> = ({ )} + ); }; diff --git a/src/server/services/vestingApi.ts b/src/server/services/vestingApi.ts index 3631528..426e322 100644 --- a/src/server/services/vestingApi.ts +++ b/src/server/services/vestingApi.ts @@ -121,7 +121,7 @@ export const coinectaVestingApi = { ): Promise { try { const response = await vestingApi.post( - "/api/v1/treasury/claimentries", + "/api/v1/treasury/claim/entries", request.addresses, ); return response.data as ClaimEntriesResponse[]; From 57fec44732aacb0f2f166fd362714b829d2a8234 Mon Sep 17 00:00:00 2001 From: Christian Benedict Gantuangco Date: Thu, 26 Sep 2024 02:24:21 +0800 Subject: [PATCH 12/18] feat: display only the multi asset and exclude the ADA coin --- src/components/WalletSelectDropdown.tsx | 2 +- .../vesting/VestingPositionTable.tsx | 26 ++++++++++--------- src/server/services/vestingApi.ts | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/WalletSelectDropdown.tsx b/src/components/WalletSelectDropdown.tsx index 256bf0e..119844c 100644 --- a/src/components/WalletSelectDropdown.tsx +++ b/src/components/WalletSelectDropdown.tsx @@ -81,7 +81,7 @@ const WalletSelectDropdown: FC = ({ onWalletDropDownUpdat if (onWalletDropDownUpdate !== undefined) { onWalletDropDownUpdate(selectedAddresses); } - }, [selectedAddresses]) + }, [selectedAddresses, onWalletDropDownUpdate]) useEffect(() => { const execute = async () => { diff --git a/src/components/vesting/VestingPositionTable.tsx b/src/components/vesting/VestingPositionTable.tsx index cca7f36..b3d5e75 100644 --- a/src/components/vesting/VestingPositionTable.tsx +++ b/src/components/vesting/VestingPositionTable.tsx @@ -79,17 +79,21 @@ const mapClaimEntriesResponseToClaimEntries = ( ): ClaimEntry[] => { return claimEntriesResponsesWithWalletType.map((entry) => { const total = entry.claimEntry.vestingValue - ? Object.values(entry.claimEntry.vestingValue).reduce( - (acc, val) => - acc + Object.values(val).reduce((sum, num) => sum + num, 0), + ? Object.entries(entry.claimEntry.vestingValue) + .filter(([policyId]) => policyId.length > 0) + .reduce( + (acc, [, assets]) => + acc + Object.values(assets).reduce((sum, num) => sum + num, 0), 0, ) : "N/A"; const claimable = entry.claimEntry.directValue - ? Object.values(entry.claimEntry.directValue).reduce( - (acc, val) => - acc + Object.values(val).reduce((sum, num) => sum + num, 0), + ? Object.entries(entry.claimEntry.directValue) + .filter(([policyId]) => policyId.length > 0) + .reduce( + (acc, [, assets]) => + acc + Object.values(assets).reduce((sum, num) => sum + num, 0), 0, ) : "N/A"; @@ -98,9 +102,9 @@ const mapClaimEntriesResponseToClaimEntries = ( id: entry.claimEntry.id, rootHash: entry.claimEntry.rootHash, ownerPkh: entry.claimEntry.claimantPkh, - token: "CNCT", // Assuming claimantPkh is used as a token here - total: typeof total === "number" ? total : "N/A", - claimable: typeof claimable === "number" ? claimable : "N/A", + token: "CNCT", + total: total, + claimable: claimable, frequency: "N/A", nextUnlockDate: "N/A", endDate: "N/A", @@ -274,10 +278,8 @@ const VestingPositionTable: FC = ({ ); setClaimEntries(mappedClaimEntries); - - console.log("Claim Entries", mappedClaimEntries); }, - [fetchClaimEntriesByAddressMutation], + [fetchClaimEntriesByAddressMutation, getWalletTypeOfAddress], ); const handleOnRedeemClick = useCallback( diff --git a/src/server/services/vestingApi.ts b/src/server/services/vestingApi.ts index 3631528..426e322 100644 --- a/src/server/services/vestingApi.ts +++ b/src/server/services/vestingApi.ts @@ -121,7 +121,7 @@ export const coinectaVestingApi = { ): Promise { try { const response = await vestingApi.post( - "/api/v1/treasury/claimentries", + "/api/v1/treasury/claim/entries", request.addresses, ); return response.data as ClaimEntriesResponse[]; From 9cde2864ed03fc1a01d0103bacf2df6d4a4ff29f Mon Sep 17 00:00:00 2001 From: Christian Benedict Gantuangco Date: Thu, 26 Sep 2024 02:33:48 +0800 Subject: [PATCH 13/18] fix: removed lovelace decimal conversion function --- .../vesting/VestingPositionTable.tsx | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/src/components/vesting/VestingPositionTable.tsx b/src/components/vesting/VestingPositionTable.tsx index 223426c..2f49963 100644 --- a/src/components/vesting/VestingPositionTable.tsx +++ b/src/components/vesting/VestingPositionTable.tsx @@ -62,22 +62,6 @@ type ClaimEntriesResponseWithWalletType = { walletType: string; }; -function convertLovelaceToAda(lovelace: number | string | undefined): string { - if (!lovelace) { - return "0.000000"; - } - - const lovelaceValue = - typeof lovelace === "string" ? parseFloat(lovelace) : lovelace; - - if (isNaN(lovelaceValue)) { - return "0.000000"; - } - - const ada = lovelaceValue / 1_000_000; - return ada.toFixed(6); -} - const rowsPerPageOptions = [5, 10, 15]; export const shortenString = (input: string): string => { @@ -255,16 +239,9 @@ const VestingPositionTable: FC = ({ claimEntriesResponsesWithWalletType, ); - const convertedClaimEntries = mappedClaimEntries.map((item) => { - return { - ...item, - claimable: convertLovelaceToAda(item.claimable), - }; - }); - - setClaimEntries(convertedClaimEntries); + setClaimEntries(mappedClaimEntries); - console.log("Claim Entries", convertedClaimEntries); + console.log("Claim Entries", mappedClaimEntries); }, [fetchClaimEntriesByAddressMutation, getWalletTypeOfAddress], ); From d0aacd35526309867f9b2ab4203b8ac6bf2edb77 Mon Sep 17 00:00:00 2001 From: kenjiebalona Date: Thu, 26 Sep 2024 03:39:00 +0800 Subject: [PATCH 14/18] feat: create decimal converter function to handle multi assets decimals and code clean up --- src/components/vesting/VestingConfirm.tsx | 5 +- .../vesting/VestingPositionTable.tsx | 95 +++++++------------ .../vesting/pages/VestingDashboardPage.tsx | 23 +++-- 3 files changed, 51 insertions(+), 72 deletions(-) diff --git a/src/components/vesting/VestingConfirm.tsx b/src/components/vesting/VestingConfirm.tsx index 3a7b49c..f334d5c 100644 --- a/src/components/vesting/VestingConfirm.tsx +++ b/src/components/vesting/VestingConfirm.tsx @@ -199,7 +199,10 @@ const VestingConfirm: FC = ({ - + @@ -240,49 +280,69 @@ const TransactionReport: FC = () => { ) } */} - { - onchainTransactions.isLoading ? ( - Loading... - ) : onchainTransactions.isError ? ( - Error fetching. - ) : ( - - - - - Address - Contribution - - Tx ID - User pool weight - - - - {onchainTransactions.data.map((item, index) => { - return ( - - - {item.address} - - {`${item.amountAda} ${getSymbol('ada')}`} - - {item.txId} - {item.userPoolWeight} - - ) - })} - -
-
- ) - } + {onchainTransactions.isLoading ? ( + Loading... + ) : onchainTransactions.isError ? ( + Error fetching. + ) : ( + + + + + Address + Receive Address + Amount + Exchange Rate + BlockChain + Transaction ID + User Pool Weight + + + + {onchainTransactions.data.map((item, index) => { + return ( + + + {item.address} + + + {item.adaReceiveAddress} + + {`${item.amount} ${item.currency}`} + {`${item.exchangeRate}`} + {`${item.blockchain}`} + + {item.txId} + + {item.userPoolWeight} + + ); + })} + +
+
+ )}
); diff --git a/src/components/admin/contribution/AcceptedCurrencies.tsx b/src/components/admin/contribution/AcceptedCurrencies.tsx index 8c5ac6f..b863a20 100644 --- a/src/components/admin/contribution/AcceptedCurrencies.tsx +++ b/src/components/admin/contribution/AcceptedCurrencies.tsx @@ -77,7 +77,7 @@ const AcceptedCurrencies: FC = ({ contributionRound, setForm, setRefetchB acceptedCurrencies: prev.acceptedCurrencies.filter(item => item.id !== id) })); addAlert('success', 'Currency removed successfully'); - setRefetchBool(prev => !prev); + // setRefetchBool(prev => !prev); }; const handleEdit = (currency: TAcceptedCurrency) => { @@ -98,7 +98,6 @@ const AcceptedCurrencies: FC = ({ contributionRound, setForm, setRefetchB })); setEditingCurrency(null); addAlert('success', 'Currency updated successfully'); - setRefetchBool(prev => !prev); } }; diff --git a/src/components/projects/contribute/ContributeTab.tsx b/src/components/projects/contribute/ContributeTab.tsx index 7262c61..8514a80 100644 --- a/src/components/projects/contribute/ContributeTab.tsx +++ b/src/components/projects/contribute/ContributeTab.tsx @@ -25,17 +25,26 @@ const ContributeTab: FC = ({ projectName, projectIcon, proje useEffect(() => { if (rounds && rounds.length > 0) { - const roundId = Number(router.query.roundId); - const index = rounds.findIndex(round => round.id === roundId); + const roundIdFromUrl = Number(router.query.roundId); + const index = rounds.findIndex(round => round.id === roundIdFromUrl); if (index !== -1) { setTabValue(index); } else { - // If no roundId in URL or invalid roundId, set URL to first round - updateUrl(0); + const nearestIndex = findNearestRoundIndex(rounds); + updateUrl(nearestIndex); } } }, [rounds, router.query.roundId]); + const findNearestRoundIndex = (rounds: TContributionRound[]) => { + const now = new Date(); + return rounds.reduce((nearestIndex, round, currentIndex, arr) => { + const nearestDiff = Math.abs(now.getTime() - new Date(arr[nearestIndex].startDate).getTime()); + const currentDiff = Math.abs(now.getTime() - new Date(round.startDate).getTime()); + return currentDiff < nearestDiff ? currentIndex : nearestIndex; + }, 0); + }; + const updateUrl = (index: number) => { if (rounds && rounds[index]) { router.push({ diff --git a/src/components/projects/contribute/SaleTerm.tsx b/src/components/projects/contribute/SaleTerm.tsx index acab17b..66f75b4 100644 --- a/src/components/projects/contribute/SaleTerm.tsx +++ b/src/components/projects/contribute/SaleTerm.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Typography, Box } from '@mui/material'; +import MarkdownRender from '@components/MarkdownRender'; interface SalesTermProps { header: string; @@ -12,9 +13,11 @@ const SalesTerm: React.FC = ({ header, bodyText }) => { {header} - - {bodyText} - + {/* */} + + + + {/* */} ); }; diff --git a/src/lib/types/types.d.ts b/src/lib/types/types.d.ts index 8c66925..6d18e56 100644 --- a/src/lib/types/types.d.ts +++ b/src/lib/types/types.d.ts @@ -152,11 +152,15 @@ interface ITransactionDetails { interface CombinedTransactionInfo { address: string; - amountAda: number; - time?: number; + adaReceiveAddress?: string; + amount: number; + currency: string; + blockchain: string; + exchangeRate?: number; dbId?: string; txId?: string; userPoolWeight?: number; + time?: number; } interface Country { diff --git a/src/server/routers/contributions.ts b/src/server/routers/contributions.ts index 774a309..135b527 100644 --- a/src/server/routers/contributions.ts +++ b/src/server/routers/contributions.ts @@ -1,9 +1,14 @@ -import { ZContributionRound } from '@lib/types/zod-schemas/contributionSchema'; -import { prisma } from '@server/prisma'; -import { TRPCError } from '@trpc/server'; -import axios from 'axios'; -import { z } from 'zod'; -import { adminProcedure, createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; +import { ZContributionRound } from "@lib/types/zod-schemas/contributionSchema"; +import { prisma } from "@server/prisma"; +import { TRPCError } from "@trpc/server"; +import axios from "axios"; +import { z } from "zod"; +import { + adminProcedure, + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "../trpc"; export const contributionRouter = createTRPCRouter({ addContributionRound: adminProcedure @@ -14,16 +19,19 @@ export const contributionRouter = createTRPCRouter({ data: { ...input, acceptedCurrencies: { - create: input.acceptedCurrencies.map(({ id, contributionRoundId, ...currencyData }) => currencyData) || [] + create: + input.acceptedCurrencies.map( + ({ id, contributionRoundId, ...currencyData }) => currencyData + ) || [], }, }, }); return newRound; } catch (error) { - console.error('Error creating contribution round:', error); + console.error("Error creating contribution round:", error); throw new TRPCError({ - message: 'Failed to create new contribution round', - code: 'INTERNAL_SERVER_ERROR', + message: "Failed to create new contribution round", + code: "INTERNAL_SERVER_ERROR", }); } }), @@ -39,7 +47,9 @@ export const contributionRouter = createTRPCRouter({ ...data, acceptedCurrencies: { deleteMany: {}, - create: acceptedCurrencies.map(({ id, contributionRoundId, ...currencyData }) => currencyData), + create: acceptedCurrencies.map( + ({ id, contributionRoundId, ...currencyData }) => currencyData + ), }, }, include: { acceptedCurrencies: true }, @@ -47,10 +57,10 @@ export const contributionRouter = createTRPCRouter({ return updatedRound; } catch (error) { - console.error('Error updating contribution round:', error); + console.error("Error updating contribution round:", error); throw new TRPCError({ message: `Failed to update contribution round with id ${id}`, - code: 'INTERNAL_SERVER_ERROR', + code: "INTERNAL_SERVER_ERROR", }); } }), @@ -63,12 +73,15 @@ export const contributionRouter = createTRPCRouter({ await prisma.contributionRound.delete({ where: { id: input.id }, }); - return { success: true, message: 'Contribution round deleted successfully' }; + return { + success: true, + message: "Contribution round deleted successfully", + }; } catch (error) { - console.error('Error deleting contribution round:', error); + console.error("Error deleting contribution round:", error); throw new TRPCError({ message: `Failed to delete contribution round with id ${input.id}`, - code: 'INTERNAL_SERVER_ERROR', + code: "INTERNAL_SERVER_ERROR", }); } }), @@ -80,34 +93,39 @@ export const contributionRouter = createTRPCRouter({ const { projectSlug } = input; const rounds = await prisma.contributionRound.findMany({ where: { projectSlug }, - orderBy: { startDate: 'asc' }, - include: { acceptedCurrencies: true } + orderBy: { startDate: "asc" }, + include: { acceptedCurrencies: true }, }); return rounds; } catch (error) { - console.error(`Error fetching contribution rounds for projectSlug ${input.projectSlug}:`, error); + console.error( + `Error fetching contribution rounds for projectSlug ${input.projectSlug}:`, + error + ); throw new TRPCError({ message: `An unexpected error occurred while fetching contribution rounds for projectSlug ${input.projectSlug}`, - code: 'INTERNAL_SERVER_ERROR', + code: "INTERNAL_SERVER_ERROR", }); } }), createTransaction: protectedProcedure - .input(z.object({ - description: z.string().optional(), - blockchain: z.string(), - adaReceiveAddress: z.string(), - exchangeRate: z.number(), - amount: z.string(), - currency: z.string(), - address: z.string(), - txId: z.string().optional(), - contributionId: z.number(), - referralCode: z.string().optional() - })) + .input( + z.object({ + description: z.string().optional(), + blockchain: z.string(), + adaReceiveAddress: z.string(), + exchangeRate: z.number(), + amount: z.string(), + currency: z.string(), + address: z.string(), + txId: z.string().optional(), + contributionId: z.number(), + referralCode: z.string().optional(), + }) + ) .mutation(async ({ input, ctx }) => { - const userId = ctx.session.user.id + const userId = ctx.session.user.id; const { adaReceiveAddress, referralCode, @@ -117,8 +135,8 @@ export const contributionRouter = createTRPCRouter({ amount, description, txId, - exchangeRate - } = input + exchangeRate, + } = input; try { const newTransaction = await prisma.transaction.create({ @@ -133,7 +151,7 @@ export const contributionRouter = createTRPCRouter({ txId, user_id: userId, contribution_id: input.contributionId, - referralCode + referralCode, }, }); @@ -143,7 +161,7 @@ export const contributionRouter = createTRPCRouter({ }, data: { deposited: { - increment: (Number(amount) / exchangeRate), + increment: Number(amount) / exchangeRate, }, }, }); @@ -153,21 +171,23 @@ export const contributionRouter = createTRPCRouter({ console.error(`Error creating transaction:`, error); throw new TRPCError({ message: `An unexpected error occurred while creating the transaction`, - code: 'INTERNAL_SERVER_ERROR', + code: "INTERNAL_SERVER_ERROR", }); } }), sumTransactions: protectedProcedure - .input(z.object({ - contributionId: z.number() - })) + .input( + z.object({ + contributionId: z.number(), + }) + ) .query(async ({ input, ctx }) => { - const userId = ctx.session.user.id + const userId = ctx.session.user.id; const transactions = await prisma.transaction.findMany({ where: { contribution_id: input.contributionId, - user_id: userId + user_id: userId, }, select: { amount: true, @@ -176,47 +196,56 @@ export const contributionRouter = createTRPCRouter({ }, }); - const totals: { amount: number; blockchain: string; currency: string }[] = []; + const totals: { amount: number; blockchain: string; currency: string }[] = + []; for (const tx of transactions) { - const existingTotal = totals.find(t => t.blockchain === tx.blockchain && t.currency === tx.currency); + const existingTotal = totals.find( + (t) => t.blockchain === tx.blockchain && t.currency === tx.currency + ); if (existingTotal) { existingTotal.amount += parseFloat(tx.amount); } else { - totals.push({ amount: parseFloat(tx.amount), blockchain: tx.blockchain || 'Cardano', currency: tx.currency }); + totals.push({ + amount: parseFloat(tx.amount), + blockchain: tx.blockchain || "Cardano", + currency: tx.currency, + }); } } - return totals.map(total => ({ + return totals.map((total) => ({ ...total, amount: Number(total.amount.toFixed(2)), })); }), listTransactionsByContribution: adminProcedure - .input(z.object({ - contributionId: z.number() - })) + .input( + z.object({ + contributionId: z.number(), + }) + ) .query(async ({ input }) => { const { contributionId } = input; const transactions = await prisma.transaction.findMany({ where: { - contribution_id: contributionId + contribution_id: contributionId, }, include: { user: { select: { defaultAddress: true, sumsubId: true, - sumsubResult: true - } - } - } + sumsubResult: true, + }, + }, + }, }); - return transactions.map(transaction => ({ + return transactions.map((transaction) => ({ ...transaction, userDefaultAddress: transaction.user?.defaultAddress, - userSumsubId: transaction.user?.sumsubId + userSumsubId: transaction.user?.sumsubId, })); }), @@ -260,9 +289,11 @@ export const contributionRouter = createTRPCRouter({ // }), contributedPoolWeight: publicProcedure - .input(z.object({ - contributionId: z.number() - })) + .input( + z.object({ + contributionId: z.number(), + }) + ) .query(async ({ input }) => { try { const { contributionId } = input; @@ -273,19 +304,23 @@ export const contributionRouter = createTRPCRouter({ include: { user: { include: { - wallets: true - } - } - } + wallets: true, + }, + }, + }, }); // Step 2: Fetch Pool Weights Data - const response = await axios.post('https://api.coinecta.fi/stake/snapshot?limit=1000', [], { - headers: { - 'Content-Type': 'application/json; charset=utf-8' + const response = await axios.post( + "https://api.coinecta.fi/stake/snapshot?limit=1000", + [], + { + headers: { + "Content-Type": "application/json; charset=utf-8", + }, } - }); - const rawResponse: IPoolWeightAPI = response.data + ); + const rawResponse: IPoolWeightAPI = response.data; const stakeData: IPoolWeightDataItem[] = rawResponse.data; // Prepare a Set to track unique addresses to avoid double-counting @@ -293,19 +328,23 @@ export const contributionRouter = createTRPCRouter({ // Step 3: Match Contributions with Staking Data let totalPoolWeight = 0; - contributions.forEach(contribution => { + contributions.forEach((contribution) => { // Check and add contribution address if not already added if (!uniqueAddresses.has(contribution.address)) { - const matchedStake = stakeData.find((stake) => stake.address === contribution.address); + const matchedStake = stakeData.find( + (stake) => stake.address === contribution.address + ); if (matchedStake) { totalPoolWeight += matchedStake.cummulativeWeight; uniqueAddresses.add(contribution.address); } } // Check and add wallet reward addresses if not already added - contribution.user.wallets.forEach(wallet => { + contribution.user.wallets.forEach((wallet) => { if (!uniqueAddresses.has(wallet.rewardAddress)) { - const matchedStake = stakeData.find(stake => stake.address === wallet.rewardAddress); + const matchedStake = stakeData.find( + (stake) => stake.address === wallet.rewardAddress + ); if (matchedStake) { totalPoolWeight += matchedStake.cummulativeWeight; uniqueAddresses.add(wallet.rewardAddress); @@ -316,86 +355,161 @@ export const contributionRouter = createTRPCRouter({ return { totalPoolWeight, - apiResponse: rawResponse + apiResponse: rawResponse, }; } catch (error) { console.error(`Error fetching pool weights`, error); throw new TRPCError({ message: `An unexpected error occurred while fetching pool weights`, - code: 'INTERNAL_SERVER_ERROR', + code: "INTERNAL_SERVER_ERROR", }); } }), getOnchainTransactions: adminProcedure - .input(z.object({ - address: z.string(), - contributionId: z.number() - })) + .input( + z.object({ + contributionId: z.number(), + }) + ) .query(async ({ input }) => { - const { address, contributionId } = input; - let combinedTransactions: CombinedTransactionInfo[] = []; - let page = 1; + const { contributionId } = input; + const combinedTransactions: CombinedTransactionInfo[] = []; - if (address && contributionId) { - try { - console.log('start') - // Fetch on-chain transactions - let allUtxos: IOnChainUtxo[] = []; - while (true) { - const response = await axios.get(`https://cardano-mainnet.blockfrost.io/api/v0/addresses/${address}/utxos`, { - params: { - count: 100, - page: page - }, - headers: { - 'project_id': process.env.BLOCKFROST_PROJECT_ID - } - }); + const stakeData = await fetchAllStakeData(); + const stakeDataMap = new Map( + stakeData.map((item) => [item.address, item]) + ); - if (response.data.length === 0) { - break; // Exit the loop if no more data - } - allUtxos = allUtxos.concat(response.data); - page++; - } + const acceptedCurrencies = await prisma.acceptedCurrency.findMany({ + where: { + contributionRoundId: contributionId, + }, + }); - // Fetch database transactions for the contribution - const dbTransactions = await prisma.transaction.findMany({ - where: { - contribution_id: contributionId - } + // BASE TX + const baseTxMap = await Promise.all( + acceptedCurrencies + .filter((acc) => acc.blockchain === "Base") + .map((acc) => acc.receiveAddress) + .filter((v, i, a) => a.indexOf(v) === i) + .map((address) => getBaseTransactions(address)) + ); + const baseTxns = baseTxMap.flatMap((x) => x); + + baseTxns.forEach(async (t) => { + const newTxData = await prisma.transaction.findFirst({ + where: { txId: t.hash }, + }); + + if (newTxData) { + await prisma.transaction.update({ + where: { id: newTxData.id }, + data: { onChainTxData: t }, }); + } else { + await prisma.newTx.create({ + data: { + onChainTxData: t, + txId: t.hash, + }, + }); + } + }); - const dbTransactionMap = new Map(dbTransactions.map(tx => [tx.txId, tx])); + // Fetch database transactions for the contribution + const dbTransactions = await prisma.transaction.findMany({ + where: { + contribution_id: contributionId, + }, + }); + const dbTransactionMap = new Map( + dbTransactions.map((tx) => [tx.txId, tx]) + ); + + const baseCombinedInfo = dbTransactions + .filter((t) => baseTxns.map((t) => t.hash).includes(t.txId ?? "")) + .map((t) => { + const txDetails = t.onChainTxData as IBaseTokenTransaction; + return { + address: txDetails.from, + adaReceiveAddress: t.adaReceiveAddress ?? "unavailable", + amount: + Number(txDetails.value) / + Math.pow(10, Number(txDetails.tokenDecimal)), + currency: t.currency, + blockchain: t.blockchain ?? "Base", + exchangeRate: Number(t.exchangeRate), + txId: t.txId ?? "unavailable", + userPoolWeight: stakeDataMap.get(txDetails.from)?.cummulativeWeight, // Assuming the input address is what we're checking pool weight against + }; + }); + + combinedTransactions.push(...baseCombinedInfo); + + const adaAddresses = acceptedCurrencies + .filter((acc) => acc.blockchain === "Cardano") + .map((acc) => acc.receiveAddress) + .filter((v, i, a) => a.indexOf(v) === i); + + if (adaAddresses.length >= 1) { + try { + // Fetch on-chain transactions + const allUtxos: IOnChainUtxo[] = (await Promise.all(adaAddresses.map(async (address) => { + let page = 1; + const utxos = []; + while (true) { + const response = await axios.get( + `https://cardano-mainnet.blockfrost.io/api/v0/addresses/${address}/utxos`, + { + params: { + count: 100, + page: page, + }, + headers: { + project_id: process.env.BLOCKFROST_PROJECT_ID, + }, + } + ); - const stakeData = await fetchAllStakeData(); - const stakeDataMap = new Map(stakeData.map(item => [item.address, item])); + if (response.data.length === 0) { + return utxos; + } + + utxos.push(...response.data); + page++; + } + }))).flatMap(x => x); for (const utxo of allUtxos) { const dbTransaction = dbTransactionMap.get(utxo.tx_hash); - let txDetails: ITransactionDetails + let txDetails: ITransactionDetails; if (dbTransaction && dbTransaction.onChainTxData) { + // We have already processed the transaction // Ensure the data is correctly parsed and structured txDetails = dbTransaction.onChainTxData as ITransactionDetails; if (!Array.isArray(txDetails.inputs)) { - console.error('Invalid or missing inputs array in the transaction details from database'); + console.error( + "Invalid or missing inputs array in the transaction details from database" + ); continue; // Skip this iteration or handle the error appropriately } - } - else { + } else { const checkDbTx = await prisma.newTx.findFirst({ - where: { txId: utxo.tx_hash } - }) + where: { txId: utxo.tx_hash }, + }); if (checkDbTx) { - txDetails = checkDbTx.onChainTxData as ITransactionDetails + txDetails = checkDbTx.onChainTxData as ITransactionDetails; } else { - const txDetailResponse = await axios.get(`https://cardano-mainnet.blockfrost.io/api/v0/txs/${utxo.tx_hash}/utxos`, { - headers: { - 'project_id': process.env.BLOCKFROST_PROJECT_ID + const txDetailResponse = await axios.get( + `https://cardano-mainnet.blockfrost.io/api/v0/txs/${utxo.tx_hash}/utxos`, + { + headers: { + project_id: process.env.BLOCKFROST_PROJECT_ID, + }, } - }); + ); txDetails = txDetailResponse.data; const newTxData = await prisma.transaction.findFirst({ @@ -406,36 +520,82 @@ export const contributionRouter = createTRPCRouter({ await prisma.transaction.update({ where: { id: newTxData.id }, data: { onChainTxData: txDetails }, - }) + }); } else { await prisma.newTx.create({ data: { onChainTxData: txDetails, - txId: utxo.tx_hash - } - }) + txId: utxo.tx_hash, + }, + }); } if (!Array.isArray(txDetails.inputs)) { - console.error('Invalid or missing inputs array in the transaction details from API'); + console.error( + "Invalid or missing inputs array in the transaction details from API" + ); continue; // Skip this iteration or handle the error appropriately } } } - let totalAdaOutputToAddress = 0; - txDetails.outputs.forEach(output => { - if (output.address === address) { - totalAdaOutputToAddress += output.amount.filter(a => a.unit === "lovelace").reduce((sum, current) => sum + Number(current.quantity) * 0.000001, 0); - } - }); + const totalAdaOutputToAddress = txDetails.outputs + .filter((output) => adaAddresses.includes(output.address)) + .map((output) => + output.amount + .filter((a) => a.unit === "lovelace") + .reduce( + (sum, current) => sum + Number(current.quantity) * 0.000001, + 0 + ) + ) + .reduce((a, c) => a + c, 0).toFixed(6); + + const totalUSDMOutputToAddress = txDetails.outputs + .filter((output) => adaAddresses.includes(output.address)) + .map((output) => + output.amount + .filter( + (a) => + a.unit === + "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad0014df105553444d" // USDM + ) + .reduce( + (sum, current) => sum + Number(current.quantity) * 0.000001, + 0 + ) + ) + .reduce((a, c) => a + c, 0).toFixed(6); + + if (Number(totalAdaOutputToAddress) > 0) { + // Only consider transactions that have outputs to the specified address + const combinedInfo = { + address: txDetails.inputs[0].address, + adaReceiveAddress: txDetails.inputs[0].address, + amount: Number(totalAdaOutputToAddress), + currency: dbTransactionMap.get(utxo.tx_hash)?.currency ?? "ADA", + exchangeRate: Number(dbTransactionMap.get(utxo.tx_hash)?.exchangeRate), + blockchain: dbTransactionMap.get(utxo.tx_hash)?.blockchain ?? "Cardano", + txId: utxo.tx_hash, + userPoolWeight: stakeDataMap.get(txDetails.inputs[0].address) + ?.cummulativeWeight, // Assuming the input address is what we're checking pool weight against + }; - if (totalAdaOutputToAddress > 0) { // Only consider transactions that have outputs to the specified address + combinedTransactions.push(combinedInfo); + } + + if (Number(totalUSDMOutputToAddress) > 0) { + // Only consider transactions that have outputs to the specified address const combinedInfo = { address: txDetails.inputs[0].address, - amountAda: totalAdaOutputToAddress, + adaReceiveAddress: txDetails.inputs[0].address, + amount: Number(totalUSDMOutputToAddress), + currency: dbTransactionMap.get(utxo.tx_hash)?.currency ?? "USDM", + exchangeRate: Number(dbTransactionMap.get(utxo.tx_hash)?.exchangeRate), + blockchain: dbTransactionMap.get(utxo.tx_hash)?.blockchain ?? "Cardano", txId: utxo.tx_hash, - userPoolWeight: stakeDataMap.get(txDetails.inputs[0].address)?.cummulativeWeight // Assuming the input address is what we're checking pool weight against + userPoolWeight: stakeDataMap.get(txDetails.inputs[0].address) + ?.cummulativeWeight, // Assuming the input address is what we're checking pool weight against }; combinedTransactions.push(combinedInfo); @@ -444,50 +604,69 @@ export const contributionRouter = createTRPCRouter({ return combinedTransactions; } catch (error) { - console.error('Error fetching transactions', error); + console.error("Error fetching transactions", error); throw new TRPCError({ - message: 'An unexpected error occurred while fetching transactions', - code: 'INTERNAL_SERVER_ERROR', + message: "An unexpected error occurred while fetching transactions", + code: "INTERNAL_SERVER_ERROR", }); } - } - else return [] + } else return combinedTransactions; }), -}) - -async function fetchAllUtxos(address: string) { - let allUtxos: any[] = []; - let page = 1; - while (true) { - const response = await axios.get(`https://cardano-mainnet.blockfrost.io/api/v0/addresses/${address}/utxos`, { - params: { count: 100, page }, - headers: { 'project_id': process.env.BLOCKFROST_PROJECT_ID } - }); - if (response.data.length === 0) break; - allUtxos = allUtxos.concat(response.data); - page++; - } - return allUtxos; -} +}); async function fetchAllStakeData() { - const response = await axios.post('https://api.coinecta.fi/stake/snapshot?limit=1000', [], { - headers: { 'Content-Type': 'application/json; charset=utf-8' } - }); + const response = await axios.post( + "https://api.coinecta.fi/stake/snapshot?limit=1000", + [], + { + headers: { "Content-Type": "application/json; charset=utf-8" }, + } + ); return response.data.data as IPoolWeightDataItem[]; } -async function fetchTransactionDetails(utxo: IOnChainUtxo, dbTransactionMap: Map) { - let txDetails: ITransactionDetails; - const dbTransaction = dbTransactionMap.get(utxo.tx_hash); - if (dbTransaction && dbTransaction.onChainTxData) { - txDetails = dbTransaction.onChainTxData as ITransactionDetails; - } else { - const response = await axios.get(`https://cardano-mainnet.blockfrost.io/api/v0/txs/${utxo.tx_hash}/utxos`, { - headers: { 'project_id': process.env.BLOCKFROST_PROJECT_ID } - }); - txDetails = response.data; - // Optionally update the transaction with onChainTxData if needed +interface IBaseTokenTransaction { + [key: string]: string; + blockNumber: string; + timeStamp: string; + hash: string; + nonce: string; + blockHash: string; + from: string; + contractAddress: string; + to: string; + value: string; + tokenName: string; + tokenSymbol: string; + tokenDecimal: string; + transactionIndex: string; + gas: string; + gasPrice: string; + gasUsed: string; + cumulativeGasUsed: string; + input: string; + confirmations: string; +} + +const getBaseTransactions = async (address: string) => { + const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + const txIds: IBaseTokenTransaction[] = []; + let page = 1; + while (true) { + try { + const response = await axios.get( + `https://api.basescan.org/api?module=account&action=tokentx&address=${address}&page=${page}&offset=1000&sort=asc&apikey=${process.env.BASE_API_KEY}` + ); + const tx = response.data.result; + if (tx.length === 0) { + return txIds; + } + txIds.push(...tx); + } catch { + throw new Error("Unexpected Error from Base"); + } + page += 1; + await delay(200); } - return txDetails; -} \ No newline at end of file +}; From 9454570f4d997fc05c93a8991aff916d5d83c19d Mon Sep 17 00:00:00 2001 From: Christian Benedict Gantuangco Date: Thu, 17 Oct 2024 06:20:58 +0800 Subject: [PATCH 18/18] refactor: added support for claiming using staking key --- src/components/vesting/VestingConfirm.tsx | 46 ++++++++++++++--------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/components/vesting/VestingConfirm.tsx b/src/components/vesting/VestingConfirm.tsx index 1787da2..e156319 100644 --- a/src/components/vesting/VestingConfirm.tsx +++ b/src/components/vesting/VestingConfirm.tsx @@ -1,7 +1,8 @@ import DataSpread from "@components/DataSpread"; import { useAlert } from "@contexts/AlertContext"; import { trpc } from "@lib/utils/trpc"; -import { useWallet } from "@meshsdk/react"; +import { BrowserWallet } from "@meshsdk/core"; +import { useWallet, useWalletList } from "@meshsdk/react"; import CloseIcon from "@mui/icons-material/Close"; import { Button, @@ -14,7 +15,7 @@ import { useMediaQuery, useTheme, } from "@mui/material"; -import React, { FC, useState, useCallback } from "react"; +import React, { FC, useState, useCallback, useEffect } from "react"; type ClaimEntry = { id: string; @@ -55,6 +56,22 @@ const VestingConfirm: FC = ({ const submitClaimTreasuryTransaction = trpc.vesting.submitClaimTreasuryTransaction.useMutation(); + const getStakeAddress = useCallback(async (walletType: string) => { + const wallet = await BrowserWallet.enable(walletType); + + const addresses = await wallet.getRewardAddresses(); + + return addresses[0]; + }, []); + + const getPaymentAddress = useCallback(async (walletType: string) => { + const wallet = await BrowserWallet.enable(walletType); + + const addresses = await wallet.getUsedAddresses(); + + return addresses[0]; + }, []); + const getRawUtxos = useCallback(async (walletType: string) => { const api = await window.cardano[walletType].enable(); @@ -84,7 +101,7 @@ const VestingConfirm: FC = ({ const handleClose = () => setOpen(false); - const handleSubmit = useCallback(async () => { + const handleSubmit = async () => { setIsSigning(true); try { if (claimEntry === null) return; @@ -95,12 +112,16 @@ const VestingConfirm: FC = ({ if (rawUtxos === undefined || rawCollateralUtxo === undefined) return; const rootHash: string = claimEntry.rootHash; - const ownerAddress: string = claimEntry.ownerAddress; + const paymentAddress = await getPaymentAddress(walletType); + const stakeAddress = await getStakeAddress(walletType); + + if (stakeAddress === undefined) return; + const { updatedRootHash, rawProof, rawClaimEntry } = await createClaimTreasuryDataMutation.mutateAsync({ rootHash, - ownerAddress, + ownerAddress: stakeAddress, }); const id: string = claimEntry.id; @@ -108,7 +129,7 @@ const VestingConfirm: FC = ({ const { unsignedTxRaw, treasuryUtxoRaw } = await claimTreasuryMutation.mutateAsync({ id, - ownerAddress, + ownerAddress: paymentAddress, updatedRootHash, rawProof, rawClaimEntry, @@ -141,18 +162,7 @@ const VestingConfirm: FC = ({ console.error("Error adding stake", ex); } setIsSigning(false); - }, [ - getRawUtxos, - getRawCollateralUtxo, - createClaimTreasuryDataMutation, - signTx, - finalizeTransaction, - claimTreasuryMutation, - submitClaimTreasuryTransaction, - claimEntry, - addAlert, - setOpen, - ]); + }; return ( <>