Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: subgraph vesting data #1886

Merged
merged 8 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading