From f94879c981ff364f11e4060aed835af6b755287d Mon Sep 17 00:00:00 2001 From: lemu Date: Tue, 6 Aug 2024 15:05:13 -0300 Subject: [PATCH] chore: subgraph vesting data (#1886) * chore: add vesting subgraph client * chore: subgraph vesting data parsing WIP * chore: subgraph vesting data parsing: cliff and pauses * chore: use latest pause log for time vested calculation * chore: replace alchemy vestings usage for subgraph * chore: replace VESTINGS_QUERY_ENDPOINT with published version * refactor: remove console logs * refactor: address pr comments --- src/clients/VestingData.ts | 41 ++++--- src/clients/VestingSubgraphTypes.ts | 35 ++++++ src/clients/VestingsSubgraph.ts | 139 +++++++++++++++++++++ src/entities/Proposal/utils.ts | 1 + src/entities/Snapshot/constants.ts | 1 + src/entities/Updates/model.test.ts | 4 +- src/routes/vestings.ts | 6 + src/services/ProjectService.ts | 5 +- src/services/VestingService.ts | 182 +++++++++++++++++++++++++--- src/services/update.ts | 5 +- src/utils/contracts/vesting.ts | 2 +- src/utils/projects.ts | 6 + 12 files changed, 388 insertions(+), 39 deletions(-) create mode 100644 src/clients/VestingSubgraphTypes.ts create mode 100644 src/clients/VestingsSubgraph.ts diff --git a/src/clients/VestingData.ts b/src/clients/VestingData.ts index 172c89bb8..d68a2b06c 100644 --- a/src/clients/VestingData.ts +++ b/src/clients/VestingData.ts @@ -27,15 +27,17 @@ export type Vesting = { address: string status: VestingStatus token: string + cliff: string + vestedPerPeriod: number[] } export type VestingWithLogs = Vesting & { logs: VestingLog[] } -function toISOString(seconds: number) { +export function toISOString(seconds: number) { return new Date(seconds * 1000).toISOString() } -function getVestingDates(contractStart: number, contractEndsTimestamp: number) { +export function getVestingDates(contractStart: number, contractEndsTimestamp: number) { const vestingStartAt = toISOString(contractStart) const vestingFinishAt = toISOString(contractEndsTimestamp) return { @@ -84,7 +86,7 @@ async function getVestingContractLogs(vestingAddress: string, provider: JsonRpcP return logsData } -function getInitialVestingStatus(startAt: string, finishAt: string) { +export function getInitialVestingStatus(startAt: string, finishAt: string) { const now = new Date() if (now < new Date(startAt)) { return VestingStatus.Pending @@ -102,6 +104,7 @@ async function getVestingContractDataV1( const vestingContract = new ethers.Contract(vestingAddress, VESTING_ABI, provider) const contractStart = Number(await vestingContract.start()) const contractDuration = Number(await vestingContract.duration()) + const contractCliff = Number(await vestingContract.cliff()) const contractEndsTimestamp = contractStart + contractDuration const start_at = toISOString(contractStart) const finish_at = toISOString(contractEndsTimestamp) @@ -121,15 +124,17 @@ async function getVestingContractDataV1( const token = getTokenSymbolFromAddress(tokenContractAddress.toLowerCase()) return { + cliff: toISOString(contractCliff), + vestedPerPeriod: [], ...getVestingDates(contractStart, contractEndsTimestamp), + vested: released + releasable, released, releasable, total, + token, status, start_at, finish_at, - token, - vested: released + releasable, } } @@ -140,6 +145,7 @@ async function getVestingContractDataV2( const vestingContract = new ethers.Contract(vestingAddress, VESTING_V2_ABI, provider) const contractStart = Number(await vestingContract.getStart()) const contractDuration = Number(await vestingContract.getPeriod()) + const contractCliff = Number(await vestingContract.getCliff()) + contractStart let contractEndsTimestamp = 0 const start_at = toISOString(contractStart) @@ -153,6 +159,8 @@ async function getVestingContractDataV2( finish_at = toISOString(contractEndsTimestamp) } + const vestedPerPeriod = ((await vestingContract.getVestedPerPeriod()) ?? []).map(parseContractValue) + const released = parseContractValue(await vestingContract.getReleased()) const releasable = parseContractValue(await vestingContract.getReleasable()) const total = parseContractValue(await vestingContract.getTotal()) @@ -172,26 +180,25 @@ async function getVestingContractDataV2( const token = getTokenSymbolFromAddress(tokenContractAddress) return { + cliff: toISOString(contractCliff), + vestedPerPeriod: vestedPerPeriod, ...getVestingDates(contractStart, contractEndsTimestamp), + vested: released + releasable, released, releasable, total, + token, status, start_at, finish_at, - token, - vested: released + releasable, } } -export async function getVestingWithLogs( - vestingAddress: string | null | undefined, - proposalId?: string -): Promise { - if (!vestingAddress || vestingAddress.length === 0) { - throw new Error('Unable to fetch vesting data for empty contract address') - } +export function sortByTimestamp(a: VestingLog, b: VestingLog) { + return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() +} +export async function getVestingWithLogsFromAlchemy(vestingAddress: string, proposalId?: string | undefined) { const provider = new ethers.providers.JsonRpcProvider(RpcService.getRpcUrl(ChainId.ETHEREUM_MAINNET)) try { @@ -200,7 +207,7 @@ export async function getVestingWithLogs( const [data, logs] = await Promise.all([dataPromise, logsPromise]) return { ...data, - logs: logs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()), + logs: logs.sort(sortByTimestamp), address: vestingAddress, } } catch (errorV2) { @@ -214,7 +221,7 @@ export async function getVestingWithLogs( address: vestingAddress, } } catch (errorV1) { - ErrorService.report('Unable to fetch vesting contract data', { + ErrorService.report('Unable to fetch vesting contract data from alchemy', { proposalId, errorV2: `${errorV2}`, errorV1: `${errorV1}`, @@ -225,7 +232,7 @@ export async function getVestingWithLogs( } } -function getTokenSymbolFromAddress(tokenAddress: string) { +export function getTokenSymbolFromAddress(tokenAddress: string) { switch (tokenAddress) { case '0x0f5d2fb29fb7d3cfee444a200298f468908cc942': return 'MANA' diff --git a/src/clients/VestingSubgraphTypes.ts b/src/clients/VestingSubgraphTypes.ts new file mode 100644 index 000000000..c83e6702e --- /dev/null +++ b/src/clients/VestingSubgraphTypes.ts @@ -0,0 +1,35 @@ +export type SubgraphVesting = { + id: string + version: number + duration: string + cliff: string + beneficiary: string + revoked: boolean + revocable: boolean + released: string + start: string + periodDuration: string + vestedPerPeriod: string[] + paused: boolean + pausable: boolean + stop: string + linear: boolean + token: string + owner: string + total: string + releaseLogs: SubgraphReleaseLog[] + pausedLogs: SubgraphPausedLog[] + revokeTimestamp: bigint +} + +type SubgraphReleaseLog = { + id: string + timestamp: string + amount: string +} + +type SubgraphPausedLog = { + id: string + timestamp: string + eventType: string +} diff --git a/src/clients/VestingsSubgraph.ts b/src/clients/VestingsSubgraph.ts new file mode 100644 index 000000000..7f8fa6d32 --- /dev/null +++ b/src/clients/VestingsSubgraph.ts @@ -0,0 +1,139 @@ +import fetch from 'isomorphic-fetch' + +import { VESTINGS_QUERY_ENDPOINT } from '../entities/Snapshot/constants' + +import { SubgraphVesting } from './VestingSubgraphTypes' +import { trimLastForwardSlash } from './utils' + +export class VestingsSubgraph { + static Cache = new Map() + private readonly queryEndpoint: string + + static from(baseUrl: string) { + baseUrl = trimLastForwardSlash(baseUrl) + if (!this.Cache.has(baseUrl)) { + this.Cache.set(baseUrl, new this(baseUrl)) + } + + return this.Cache.get(baseUrl)! + } + + static get() { + return this.from(this.getQueryEndpoint()) + } + + constructor(baseUrl: string) { + this.queryEndpoint = baseUrl + } + + private static getQueryEndpoint() { + if (!VESTINGS_QUERY_ENDPOINT) { + throw new Error( + 'Failed to determine vestings subgraph query endpoint. Please check VESTINGS_QUERY_ENDPOINT env is defined' + ) + } + return VESTINGS_QUERY_ENDPOINT + } + + async getVesting(address: string): Promise { + const query = ` + query getVesting($address: String!) { + vestings(where: { id: $address }){ + id + version + duration + cliff + beneficiary + revoked + revocable + released + start + periodDuration + vestedPerPeriod + paused + pausable + stop + linear + token + owner + total + revokeTimestamp + releaseLogs{ + id + timestamp + amount + } + pausedLogs{ + id + timestamp + eventType + } + } + } + ` + + const variables = { address } + const response = await fetch(this.queryEndpoint, { + method: 'post', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query, + variables: variables, + }), + }) + + const body = await response.json() + return body?.data?.vestings[0] || {} + } + + async getVestings(addresses: string[]): Promise { + const query = ` + query getVestings($addresses: [String]!) { + vestings(where: { id_in: $addresses }){ + id + version + duration + cliff + beneficiary + revoked + revocable + released + start + periodDuration + vestedPerPeriod + paused + pausable + stop + linear + token + owner + total + revokeTimestamp + releaseLogs{ + id + timestamp + amount + } + pausedLogs{ + id + timestamp + eventType + } + } + } + ` + + const variables = { addresses } + const response = await fetch(this.queryEndpoint, { + method: 'post', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query, + variables: variables, + }), + }) + + const body = await response.json() + return body?.data?.vestings || [] + } +} diff --git a/src/entities/Proposal/utils.ts b/src/entities/Proposal/utils.ts index 6d7facb78..f71c5d99d 100644 --- a/src/entities/Proposal/utils.ts +++ b/src/entities/Proposal/utils.ts @@ -40,6 +40,7 @@ export const SITEMAP_ITEMS_PER_PAGE = 100 export const DEFAULT_CHOICES = ['yes', 'no', 'abstain'] export const REGEX_NAME = new RegExp(`^([a-zA-Z0-9]){${MIN_NAME_SIZE},${MAX_NAME_SIZE}}$`) +//TODO: avoid manually calculating cliff, use subgraph or contract method instead export const CLIFF_PERIOD_IN_DAYS = 29 export function formatBalance(value: number | bigint) { diff --git a/src/entities/Snapshot/constants.ts b/src/entities/Snapshot/constants.ts index a9d17d43a..1c371ab7b 100644 --- a/src/entities/Snapshot/constants.ts +++ b/src/entities/Snapshot/constants.ts @@ -9,3 +9,4 @@ export const SNAPSHOT_DURATION = Number(process.env.GATSBY_SNAPSHOT_DURATION || export const SNAPSHOT_URL = process.env.GATSBY_SNAPSHOT_URL || 'https://testnet.snapshot.org/' export const SNAPSHOT_QUERY_ENDPOINT = `https://gateway-arbitrum.network.thegraph.com/api/${process.env.THE_GRAPH_API_KEY}/subgraphs/id/4YgtogVaqoM8CErHWDK8mKQ825BcVdKB8vBYmb4avAQo` export const SNAPSHOT_API = process.env.GATSBY_SNAPSHOT_API || '' +export const VESTINGS_QUERY_ENDPOINT = `https://gateway-arbitrum.network.thegraph.com/api/${process.env.THE_GRAPH_API_KEY}/subgraphs/id/Dek4AeCYyGQ8Y2yeVNb2N7cfQDy7Pinka1jD5uWvRCxG` diff --git a/src/entities/Updates/model.test.ts b/src/entities/Updates/model.test.ts index 3dd6ac642..db9849646 100644 --- a/src/entities/Updates/model.test.ts +++ b/src/entities/Updates/model.test.ts @@ -1,9 +1,9 @@ import crypto from 'crypto' -import * as VestingUtils from '../../clients/VestingData' import { VestingWithLogs } from '../../clients/VestingData' import { Project } from '../../models/Project' import { ProjectService } from '../../services/ProjectService' +import { VestingService } from '../../services/VestingService' import { UpdateService } from '../../services/update' import Time from '../../utils/date/Time' import { getMonthsBetweenDates } from '../../utils/date/getMonthsBetweenDates' @@ -31,7 +31,7 @@ const MOCK_PROJECT: Project = { } function mockVestingData(vestingDates: VestingWithLogs) { - jest.spyOn(VestingUtils, 'getVestingWithLogs').mockResolvedValue(vestingDates) + jest.spyOn(VestingService, 'getVestingWithLogs').mockResolvedValue(vestingDates) } describe('UpdateModel', () => { diff --git a/src/routes/vestings.ts b/src/routes/vestings.ts index a80d25cdd..085a0156c 100644 --- a/src/routes/vestings.ts +++ b/src/routes/vestings.ts @@ -9,6 +9,7 @@ import { validateAddress } from '../utils/validations' export default routes((router) => { router.get('/all-vestings', handleAPI(getAllVestings)) router.post('/vesting', handleAPI(getVestings)) + router.get('/vesting/:address', handleAPI(getVesting)) }) async function getAllVestings() { @@ -21,3 +22,8 @@ async function getVestings(req: Request) { + const address = validateAddress(req.params.address) + return await VestingService.getVestingWithLogs(address) +} diff --git a/src/services/ProjectService.ts b/src/services/ProjectService.ts index 973dfb2a0..b7e825d77 100644 --- a/src/services/ProjectService.ts +++ b/src/services/ProjectService.ts @@ -1,7 +1,6 @@ import crypto from 'crypto' import { TransparencyVesting } from '../clients/Transparency' -import { getVestingWithLogs } from '../clients/VestingData' import UnpublishedBidModel from '../entities/Bid/model' import { BidProposalConfiguration } from '../entities/Bid/types' import { GrantTier } from '../entities/Grant/GrantTier' @@ -58,7 +57,7 @@ export class ProjectService { proposalVestings.find( (vesting) => vesting.vesting_status === VestingStatus.InProgress || vesting.vesting_status === VestingStatus.Finished - ) || proposalVestings[0] + ) || proposalVestings[0] //TODO: replace transparency vestings for vestings subgraph const project = createProposalProject(proposal, prioritizedVesting) try { @@ -246,7 +245,7 @@ export class ProjectService { private static async updateStatusFromVesting(project: Project) { try { const latestVesting = project.vesting_addresses[project.vesting_addresses.length - 1] - const vestingWithLogs = await getVestingWithLogs(latestVesting) + const vestingWithLogs = await VestingService.getVestingWithLogs(latestVesting) const updatedProjectStatus = toGovernanceProjectStatus(vestingWithLogs.status) await ProjectModel.update({ status: updatedProjectStatus, updated_at: new Date() }, { id: project.id }) diff --git a/src/services/VestingService.ts b/src/services/VestingService.ts index 6bc77667a..d936872a4 100644 --- a/src/services/VestingService.ts +++ b/src/services/VestingService.ts @@ -1,7 +1,23 @@ import { Transparency, TransparencyVesting } from '../clients/Transparency' -import { VestingWithLogs, getVestingWithLogs } from '../clients/VestingData' +import { + Vesting, + VestingLog, + VestingWithLogs, + getInitialVestingStatus, + getTokenSymbolFromAddress, + getVestingDates, + getVestingWithLogsFromAlchemy, + sortByTimestamp, + toISOString, +} from '../clients/VestingData' +import { SubgraphVesting } from '../clients/VestingSubgraphTypes' +import { VestingsSubgraph } from '../clients/VestingsSubgraph' +import { VestingStatus } from '../entities/Grant/types' +import { ContractVersion, TopicsByVersion } from '../utils/contracts/vesting' +import { ErrorCategory } from '../utils/errorCategories' import CacheService, { TTL_24_HS } from './CacheService' +import { ErrorService } from './ErrorService' export class VestingService { static async getAllVestings() { @@ -18,27 +34,165 @@ export class VestingService { } static async getVestings(addresses: string[]): Promise { - const vestings = await Promise.all(addresses.map((address) => getVestingWithLogs(address))) + const vestingsData = await VestingsSubgraph.get().getVestings(addresses) + return vestingsData.map(this.parseSubgraphVesting).sort(this.sortVestingsByDate) + } + + static async getVestingWithLogs( + vestingAddress: string | null | undefined, + proposalId?: string + ): Promise { + if (!vestingAddress || vestingAddress.length === 0) { + throw new Error('Unable to fetch vesting data for empty contract address') + } - return vestings.sort(compareVestingInfo) + try { + return await this.getVestingWithLogsFromSubgraph(vestingAddress, proposalId) + } catch (error) { + return await getVestingWithLogsFromAlchemy(vestingAddress, proposalId) + } + } + + private static async getVestingWithLogsFromSubgraph( + vestingAddress: string, + proposalId?: string + ): Promise { + try { + const subgraphVesting = await VestingsSubgraph.get().getVesting(vestingAddress) + return this.parseSubgraphVesting(subgraphVesting) + } catch (error) { + ErrorService.report('Unable to fetch vestings subgraph data', { + error, + vestingAddress, + proposalId, + category: ErrorCategory.Vesting, + }) + throw error + } } -} -function compareVestingInfo(a: VestingWithLogs, b: VestingWithLogs): number { - if (a.logs.length === 0 && b.logs.length === 0) { - return new Date(b.start_at).getTime() - new Date(a.start_at).getTime() + private static parseSubgraphVesting(vestingData: SubgraphVesting) { + const vestingContract = this.parseVestingData(vestingData) + const logs = this.parseVestingLogs(vestingData) + return { ...vestingContract, logs } } - if (a.logs.length === 0) { - return -1 + private static parseVestingData(vestingData: SubgraphVesting): Vesting { + const contractStart = Number(vestingData.start) + const contractDuration = Number(vestingData.duration) + const cliffEnd = Number(vestingData.cliff) + const currentTime = Math.floor(Date.now() / 1000) + + const start_at = toISOString(contractStart) + const contractEndsTimestamp = contractStart + contractDuration + const finish_at = toISOString(contractEndsTimestamp) + + const released = Number(vestingData.released) + const total = Number(vestingData.total) + let vested = 0 + + if (currentTime < cliffEnd) { + // If we're before the cliff end, nothing is vested + vested = 0 + } else if (vestingData.linear) { + // Linear vesting after the cliff + if (currentTime >= contractEndsTimestamp) { + vested = total + } else { + const timeElapsed = currentTime - contractStart + vested = (timeElapsed / contractDuration) * total + } + } else { + // Periodic vesting after the cliff + const periodDuration = Number(vestingData.periodDuration) + let timeVested = currentTime - contractStart + + // Adjust for pauses (we only use the latest pause log. If unpaused, it resumes as if it'd have never been paused) + if (vestingData.paused) { + if (vestingData.pausedLogs && vestingData.pausedLogs.length > 0) { + const latestPauseLog = vestingData.pausedLogs.reduce((latestLog, currentLog) => { + return Number(currentLog.timestamp) > Number(latestLog.timestamp) ? currentLog : latestLog + }, vestingData.pausedLogs[0]) + const pauseTimestamp = Number(latestPauseLog.timestamp) + if (currentTime >= pauseTimestamp) { + timeVested = pauseTimestamp - contractStart + } + } + } + + const periodsCompleted = Math.floor(timeVested / periodDuration) + + // Sum vested tokens for completed periods + for (let i = 0; i < periodsCompleted && i < vestingData.vestedPerPeriod.length; i++) { + vested += Number(vestingData.vestedPerPeriod[i]) + } + } + + const releasable = vested - released + + let status = getInitialVestingStatus(start_at, finish_at) + if (vestingData.revoked) { + status = VestingStatus.Revoked + } else if (vestingData.paused) { + status = VestingStatus.Paused + } + + const token = getTokenSymbolFromAddress(vestingData.token) + + return { + address: vestingData.id, + cliff: toISOString(cliffEnd), + vestedPerPeriod: vestingData.vestedPerPeriod.map(Number), + ...getVestingDates(contractStart, contractEndsTimestamp), + vested, + released, + releasable, + total, + token, + status, + start_at, + finish_at, + } } - if (b.logs.length === 0) { - return 1 + private static parseVestingLogs(vestingData: SubgraphVesting) { + const version = vestingData.linear ? ContractVersion.V1 : ContractVersion.V2 + const topics = TopicsByVersion[version] + const logs: VestingLog[] = [] + const parsedReleases: VestingLog[] = vestingData.releaseLogs.map((releaseLog) => { + return { + topic: topics.RELEASE, + timestamp: toISOString(Number(releaseLog.timestamp)), + amount: Number(releaseLog.amount), + } + }) + logs.push(...parsedReleases) + const parsedPauseEvents: VestingLog[] = vestingData.pausedLogs.map((pausedLog) => { + return { + topic: pausedLog.eventType === 'Paused' ? topics.PAUSED : topics.UNPAUSED, + timestamp: toISOString(Number(pausedLog.timestamp)), + } + }) + logs.push(...parsedPauseEvents) + return logs.sort(sortByTimestamp) } - const aLatestLogTimestamp = new Date(a.logs[0].timestamp).getTime() - const bLatestLogTimestamp = new Date(b.logs[0].timestamp).getTime() + private static sortVestingsByDate(a: VestingWithLogs, b: VestingWithLogs): number { + if (a.logs.length === 0 && b.logs.length === 0) { + return new Date(b.start_at).getTime() - new Date(a.start_at).getTime() + } + + if (a.logs.length === 0) { + return -1 + } - return bLatestLogTimestamp - aLatestLogTimestamp + if (b.logs.length === 0) { + return 1 + } + + const aLatestLogTimestamp = new Date(a.logs[0].timestamp).getTime() + const bLatestLogTimestamp = new Date(b.logs[0].timestamp).getTime() + + return bLatestLogTimestamp - aLatestLogTimestamp + } } diff --git a/src/services/update.ts b/src/services/update.ts index 337c18e94..bd8c4bbc9 100644 --- a/src/services/update.ts +++ b/src/services/update.ts @@ -3,7 +3,7 @@ import logger from 'decentraland-gatsby/dist/entities/Development/logger' import RequestError from 'decentraland-gatsby/dist/entities/Route/error' import { Discourse } from '../clients/Discourse' -import { VestingWithLogs, getVestingWithLogs } from '../clients/VestingData' +import { VestingWithLogs } from '../clients/VestingData' import { ProposalAttributes } from '../entities/Proposal/types' import UpdateModel from '../entities/Updates/model' import { UpdateAttributes, UpdateStatus } from '../entities/Updates/types' @@ -24,6 +24,7 @@ import Time from '../utils/date/Time' import { getMonthsBetweenDates } from '../utils/date/getMonthsBetweenDates' import { ErrorCategory } from '../utils/errorCategories' +import { VestingService } from './VestingService' import { DiscordService } from './discord' import { EventsService } from './events' @@ -194,7 +195,7 @@ export class UpdateService { const project = await ProjectService.getUpdatedProject(projectId) const { vesting_addresses, proposal_id } = project const vestingAddresses = initialVestingAddresses || vesting_addresses - const vesting = await getVestingWithLogs(vestingAddresses[vestingAddresses.length - 1], proposal_id) + const vesting = await VestingService.getVestingWithLogs(vestingAddresses[vestingAddresses.length - 1], proposal_id) const now = new Date() const updatesQuantity = this.getAmountOfUpdates(vesting) diff --git a/src/utils/contracts/vesting.ts b/src/utils/contracts/vesting.ts index 3251db072..3d6cd5a6e 100644 --- a/src/utils/contracts/vesting.ts +++ b/src/utils/contracts/vesting.ts @@ -3,7 +3,7 @@ export enum ContractVersion { V2 = 'v2', } -type Topics = { +export type Topics = { RELEASE: string REVOKE: string TRANSFER_OWNERSHIP: string diff --git a/src/utils/projects.ts b/src/utils/projects.ts index 013c887ca..c8a18e6bd 100644 --- a/src/utils/projects.ts +++ b/src/utils/projects.ts @@ -2,6 +2,9 @@ import { TransparencyVesting } from '../clients/Transparency' import { Vesting } from '../clients/VestingData' import { ProjectStatus, VestingStatus } from '../entities/Grant/types' import { ProjectFunding, ProposalAttributes, ProposalProject, ProposalWithProject } from '../entities/Proposal/types' +import { CLIFF_PERIOD_IN_DAYS } from '../entities/Proposal/utils' + +import Time from './date/Time' export function getHighBudgetVpThreshold(budget: number) { return 1200000 + budget * 40 @@ -83,6 +86,7 @@ export function createProposalProject(proposal: ProposalWithProject, vesting?: T } } +//TODO: stop using transparency vestings export function toVesting(transparencyVesting: TransparencyVesting): Vesting { const { token, @@ -105,6 +109,8 @@ export function toVesting(transparencyVesting: TransparencyVesting): Vesting { total: Math.round(vesting_total_amount), vested: Math.round(vesting_released + vesting_releasable), status: vesting_status, + cliff: Time.unix(Number(vesting_start_at)).add(CLIFF_PERIOD_IN_DAYS, 'day').getTime().toString(), + vestedPerPeriod: [], } return vesting