-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat/add vesting backend and frontend (#134)
* add new routing for getEvents * add new logic for getEvents to return vesting * add env.example file * change operant * refactoring * add missing param * add caching and start_block * update package-lock.json * add new logic * add annotation for types * update routes * rework get vesting event * change governance contract * update logic to fetch all events * refactoring * add new package * add new VestingTable * add VestingTable * add basic url * fix api url * refactoring caching * add new table style * unify BASE_API_URL * add do .. while * Refactor backend for readability * Polish refactor * remove IsVestingMilestones if it is vested * refactoring getVestingEvents * Enforce consistency and code quality --------- Co-authored-by: Ondřej Sojka <[email protected]>
- Loading branch information
Showing
10 changed files
with
446 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Env variables | ||
STARKNET_RPC=# | ||
# If network == `mainnet` it will use StarknetChainId.SN_MAIN | ||
# otherwise it will use by default StarknetChainId.SN_SEPOLIA | ||
NETWORK=# |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { RpcProvider, hash } from "starknet"; | ||
|
||
|
||
interface CacheEntry { | ||
events: any[], | ||
lastBlock: number | ||
} | ||
|
||
const cache: Map<string, CacheEntry> = new Map(); | ||
|
||
export const getRawVestingEvents = async (contract: string, provider: RpcProvider, fromBlock: number): Promise<any[]> => { | ||
const latestBlockPromise = provider.getBlockLatestAccepted(); | ||
let cached = getCachedVestingEvents(contract, fromBlock); | ||
|
||
const newlyFetched = await fetchVestingEvents(contract, provider, cached.lastBlock); | ||
const events = [...cached.events, ...newlyFetched]; | ||
console.log('saving ', newlyFetched.length, ' newly fetched to cache'); | ||
cache.set(`${contract}-${fromBlock}`, { events, lastBlock: (await latestBlockPromise).block_number }); | ||
return events; | ||
}; | ||
|
||
const getCachedVestingEvents = (contract: string, fromBlock: number): CacheEntry => { | ||
const cachedEntry = cache.get(`${contract}-${fromBlock}`) | ||
if (cachedEntry === undefined) { | ||
return { lastBlock: fromBlock, events: [] } | ||
}; | ||
let nextEntry = getCachedVestingEvents(contract, cachedEntry.lastBlock); | ||
return { lastBlock: nextEntry.lastBlock, events: [...cachedEntry.events, ...nextEntry.events] } | ||
} | ||
|
||
const fetchVestingEvents = async (contract: string, provider: RpcProvider, fromBlock: number, continuation_token?: string) => { | ||
const eventFilter = { | ||
from_block: { block_number: fromBlock }, | ||
chunk_size: 100, | ||
address: contract, | ||
keys: [[hash.getSelectorFromName('VestingEvent')]], | ||
...(continuation_token && { continuation_token }) | ||
}; | ||
|
||
const events = await provider.getEvents(eventFilter); | ||
|
||
let res = events.events | ||
|
||
if (events.continuation_token) { // means we can continue, more to fetch | ||
const nextEvents = await fetchVestingEvents(contract, provider, fromBlock, events.continuation_token); | ||
res = [...res, ...nextEvents]; | ||
} | ||
|
||
return res; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,32 @@ | ||
import { Router } from "express"; | ||
import { submitProposal } from "./ipfs"; | ||
import { Router } from 'express'; | ||
import { submitProposal } from './ipfs'; | ||
import { getVestingEvents } from './starknet'; | ||
import { Request, Response } from 'express'; | ||
|
||
const router = Router(); | ||
|
||
router.post("/submit", submitProposal); | ||
// Existing route for submitting proposals | ||
router.post('/submit', submitProposal); | ||
|
||
// Vesting events for user | ||
router.get('/vesting-events', async (req: Request, res: Response) => { | ||
const { contract, address } = req.query; | ||
|
||
// Ensure that contract and address are strings, not arrays or other types | ||
if (typeof contract !== 'string' || typeof address !== 'string') { | ||
return res.status(400).json({ error: 'Both contract and address must be valid strings.' }); | ||
} | ||
|
||
console.log(`Received request for vesting events with contract: ${contract} and address: ${address}`); | ||
|
||
try { | ||
const events = await getVestingEvents(contract, address); | ||
res.json(events); | ||
} catch (error) { | ||
console.error('Error fetching vesting events:', error); | ||
res.status(500).json({ error: 'Error fetching vesting events.' }); | ||
} | ||
}); | ||
|
||
|
||
export default router; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import React, { useState } from 'react'; | ||
import { CONTRACT_ADDR } from '../lib/config'; | ||
import axios from 'axios'; | ||
import { useAccount } from '@starknet-react/core'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
import { BASE_API_URL } from "../lib/config"; | ||
|
||
interface VestingEvent { | ||
amount: number; | ||
claimable_at: number | null; | ||
is_claimable: boolean; | ||
is_claimed: boolean; | ||
} | ||
|
||
const ITEMS_PER_PAGE = 10; // Items per page | ||
|
||
const VestingTable: React.FC = () => { | ||
const { address } = useAccount(); | ||
const [currentPage, setCurrentPage] = useState(1); // Track the current page | ||
|
||
const { data: events = [], isLoading, error } = useQuery({ | ||
queryKey: ['vesting-events', address], | ||
queryFn: async () => { | ||
if (!address) { | ||
return []; | ||
} | ||
const response = await axios.get(`${BASE_API_URL}/vesting-events`, { | ||
params: { | ||
contract: CONTRACT_ADDR, | ||
address: address, | ||
} | ||
}); | ||
return response.data; | ||
}, | ||
enabled: !!address, // Only fetch if the address is available | ||
retry: false, // Disable retries for this query | ||
}); | ||
|
||
// Pagination logic | ||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; | ||
const paginatedEvents = events.slice(startIndex, startIndex + ITEMS_PER_PAGE); | ||
const totalPages = Math.ceil(events.length / ITEMS_PER_PAGE); | ||
|
||
const handleNextPage = () => { | ||
if (currentPage < totalPages) { | ||
setCurrentPage(currentPage + 1); | ||
} | ||
}; | ||
|
||
const handlePreviousPage = () => { | ||
if (currentPage > 1) { | ||
setCurrentPage(currentPage - 1); | ||
} | ||
}; | ||
|
||
if (isLoading) { | ||
return <div>Loading...</div>; | ||
} | ||
|
||
if (error) { | ||
return <p>{(error as Error).message || 'Failed to load vesting events.'}</p>; | ||
} | ||
|
||
return ( | ||
<div className="w-full max-w-[50rem] mt-4"> | ||
{/* Title */} | ||
<h2 className="text-xl font-bold mb-2">Vesting Milestones</h2> | ||
|
||
{/* Table */} | ||
<table className="w-full border-collapse"> | ||
<thead> | ||
<tr className="bg-gray-100"> | ||
<th className="border p-2 text-left">Amount</th> | ||
<th className="border p-2 text-left">Claimable At</th> | ||
<th className="border p-2 text-left">Is Claimable</th> | ||
<th className="border p-2 text-left">Is Claimed</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{paginatedEvents.map((event: VestingEvent, index: number) => ( | ||
<tr key={index} className="hover:bg-gray-50"> | ||
<td className="border p-2">{event.amount}</td> | ||
<td className="border p-2"> | ||
{event.claimable_at ? new Date(event.claimable_at * 1000).toLocaleString() : 'N/A'} | ||
</td> | ||
<td className="border p-2">{event.is_claimable ? 'Yes' : 'No'}</td> | ||
<td className="border p-2">{event.is_claimed ? 'Yes' : 'No'}</td> | ||
</tr> | ||
))} | ||
</tbody> | ||
</table> | ||
|
||
{/* Pagination Controls */} | ||
{totalPages > 1 && ( | ||
<div className="flex justify-between items-center mt-4"> | ||
<button | ||
onClick={handlePreviousPage} | ||
disabled={currentPage === 1} | ||
className={`px-4 py-2 bg-gray-300 rounded ${currentPage === 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-400'}`} | ||
> | ||
Previous | ||
</button> | ||
|
||
<span className="text-sm"> | ||
Page {currentPage} of {totalPages} | ||
</span> | ||
|
||
<button | ||
onClick={handleNextPage} | ||
disabled={currentPage === totalPages} | ||
className={`px-4 py-2 bg-gray-300 rounded ${currentPage === totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-400'}`} | ||
> | ||
Next | ||
</button> | ||
</div> | ||
)} | ||
</div> | ||
); | ||
|
||
}; | ||
|
||
export default VestingTable; |
Oops, something went wrong.