Skip to content

Commit

Permalink
chore: subgraph vesting data (#1886)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
1emu committed Aug 6, 2024
1 parent 3aefe65 commit f94879c
Show file tree
Hide file tree
Showing 12 changed files with 388 additions and 39 deletions.
41 changes: 24 additions & 17 deletions src/clients/VestingData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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,
}
}

Expand All @@ -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)
Expand All @@ -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())
Expand All @@ -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<VestingWithLogs> {
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 {
Expand All @@ -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) {
Expand All @@ -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}`,
Expand All @@ -225,7 +232,7 @@ export async function getVestingWithLogs(
}
}

function getTokenSymbolFromAddress(tokenAddress: string) {
export function getTokenSymbolFromAddress(tokenAddress: string) {
switch (tokenAddress) {
case '0x0f5d2fb29fb7d3cfee444a200298f468908cc942':
return 'MANA'
Expand Down
35 changes: 35 additions & 0 deletions src/clients/VestingSubgraphTypes.ts
Original file line number Diff line number Diff line change
@@ -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
}
139 changes: 139 additions & 0 deletions src/clients/VestingsSubgraph.ts
Original file line number Diff line number Diff line change
@@ -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<string, VestingsSubgraph>()
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<SubgraphVesting> {
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<SubgraphVesting[]> {
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 || []
}
}
1 change: 1 addition & 0 deletions src/entities/Proposal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/entities/Snapshot/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
4 changes: 2 additions & 2 deletions src/entities/Updates/model.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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', () => {
Expand Down
6 changes: 6 additions & 0 deletions src/routes/vestings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -21,3 +22,8 @@ async function getVestings(req: Request<unknown, unknown, { addresses: string[]

return await VestingService.getVestings(addresses)
}

async function getVesting(req: Request<{ address: string }>) {
const address = validateAddress(req.params.address)
return await VestingService.getVestingWithLogs(address)
}
5 changes: 2 additions & 3 deletions src/services/ProjectService.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 })

Expand Down
Loading

0 comments on commit f94879c

Please sign in to comment.