diff --git a/cli/src/index.ts b/cli/src/index.ts index 34e6351..23643dc 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -121,6 +121,7 @@ const processEpoch = async () => { number: metadata.epoch, startTimestamp: metadata.epochStartTimestamp, endTimestamp: metadata.epochEndTimestamp, + distributionTimestamp: metadata.epochEndTimestamp + 1, startBlock: Number(startBlock), endBlock: Number(endBlock), treasuryAddress: metadata.treasuryAddress, @@ -255,6 +256,7 @@ const processDistribution = async (metadata: RFOXMetadata, epoch: Epoch, wallet: processedEpoch.runePriceUsd = runePriceUsd processedEpoch.distributionStatus = 'complete' + processedEpoch.distributionTimestamp = Date.now() stakingContracts.forEach(stakingContract => { processedEpoch.detailsByStakingContract[stakingContract].assetPriceUsd = assetPriceUsd[stakingContract] }) diff --git a/cli/src/ipfs.ts b/cli/src/ipfs.ts index db84dab..dd82e40 100644 --- a/cli/src/ipfs.ts +++ b/cli/src/ipfs.ts @@ -1,6 +1,6 @@ import * as prompts from '@inquirer/prompts' import { PinataSDK } from 'pinata' -import { isAxiosError } from 'axios' +import axios, { isAxiosError } from 'axios' import BigNumber from 'bignumber.js' import { error, info } from './logging' import { Epoch, EpochDetails, RFOXMetadata, RewardDistribution } from './types' @@ -10,6 +10,8 @@ import { ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS_FOX } from './client' const PINATA_JWT = process.env['PINATA_JWT'] const PINATA_GATEWAY_URL = process.env['PINATA_GATEWAY_URL'] const PINATA_GATEWAY_API_KEY = process.env['PINATA_GATEWAY_API_KEY'] +const UNCHAINED_URL = process.env['UNCHAINED_URL'] +const UNCHAINED_V1_URL = process.env['UNCHAINED_V1_URL'] if (!PINATA_JWT) { error('PINATA_JWT not set. Please make sure you copied the sample.env and filled out your .env file.') @@ -26,6 +28,20 @@ if (!PINATA_GATEWAY_API_KEY) { process.exit(1) } +if (!UNCHAINED_URL) { + error('UNCHAINED_URL not set. Please make sure you copied the sample.env and filled out your .env file.') + process.exit(1) +} + +if (!UNCHAINED_V1_URL) { + error('UNCHAINED_V1_URL not set. Please make sure you copied the sample.env and filled out your .env file.') + process.exit(1) +} + +type Tx = { + timestamp: number +} + const isMetadata = (obj: any): obj is RFOXMetadata => { return ( obj !== null && @@ -52,6 +68,7 @@ const isEpoch = (obj: any): obj is Epoch => { typeof obj.number === 'number' && typeof obj.startTimestamp === 'number' && typeof obj.endTimestamp === 'number' && + typeof obj.distributionTimestamp === 'number' && typeof obj.startBlock === 'number' && typeof obj.endBlock === 'number' && typeof obj.treasuryAddress === 'string' && @@ -372,74 +389,141 @@ export class IPFS { return epoch } + private async migrate_v1(data: any) { + const metadata = { + epoch: data.epoch, + epochStartTimestamp: data.epochStartTimestamp, + epochEndTimestamp: data.epochEndTimestamp, + treasuryAddress: data.treasuryAddress, + burnRate: data.burnRate, + distributionRateByStakingContract: { + [ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS_FOX]: data.distributionRate, + }, + ipfsHashByEpoch: {} as Record, + } + + for (const [epochNum, epochHash] of Object.entries(data.ipfsHashByEpoch)) { + const { data } = (await this.client.gateways.public.get(epochHash)) as any + + if (isEpoch(data)) { + metadata.ipfsHashByEpoch[epochNum] = epochHash + continue + } + + const epoch = { + number: data.number, + startTimestamp: data.startTimestamp, + endTimestamp: data.endTimestamp, + startBlock: data.startBlock, + endBlock: data.endBlock, + treasuryAddress: data.treasuryAddress, + totalRevenue: data.totalRevenue, + burnRate: data.burnRate, + ...(data.runePriceUsd && { + runePriceUsd: data.runePriceUsd, + }), + distributionStatus: data.distributionStatus, + detailsByStakingContract: { + [ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS_FOX]: { + totalRewardUnits: data.totalRewardUnits, + distributionRate: data.distributionRate, + ...(data.assetPriceUsd && { + assetPriceUsd: data.assetPriceUsd, + }), + distributionsByStakingAddress: data.distributionsByStakingAddress, + }, + }, + } + + metadata.ipfsHashByEpoch[epochNum] = await this.addEpoch(epoch) + } + + return metadata + } + + async migrate_v2(data: RFOXMetadata): Promise { + const metadata: RFOXMetadata = { ...data, ipfsHashByEpoch: {} } + + for (const [epochNum, epochHash] of Object.entries(data.ipfsHashByEpoch)) { + const { data } = (await this.client.gateways.public.get(epochHash)) as any + + if (isEpoch(data)) { + metadata.ipfsHashByEpoch[epochNum] = epochHash + continue + } + + const distributions = Object.values<{ txId: string }>( + data.detailsByStakingContract[ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS_FOX].distributionsByStakingAddress, + ) + + const txid = distributions[0].txId + + if (!txid) { + metadata.ipfsHashByEpoch[epochNum] = epochHash + continue + } + + const { data: tx } = await (async () => { + try { + return await axios.get(`${UNCHAINED_URL}/api/v1/tx/${txid}`) + } catch { + return await axios.get(`${UNCHAINED_V1_URL}/api/v1/tx/${txid}`) + } + })() + + const epoch: Epoch = { + number: data.number, + startTimestamp: data.startTimestamp, + endTimestamp: data.endTimestamp, + distributionTimestamp: tx.timestamp * 1000, + startBlock: data.startBlock, + endBlock: data.endBlock, + treasuryAddress: data.treasuryAddress, + totalRevenue: data.totalRevenue, + ...(data.revenue && { revenue: data.revenue }), + burnRate: data.burnRate, + ...(data.runePriceUsd && { runePriceUsd: data.runePriceUsd }), + distributionStatus: data.distributionStatus, + detailsByStakingContract: data.detailsByStakingContract, + } + + metadata.ipfsHashByEpoch[epochNum] = await this.addEpoch(epoch) + } + + return metadata + } + async migrate(): Promise { const metadataHash = await prompts.input({ message: `What is the IPFS hash for the rFOX metadata you want to migrate? `, }) try { - const { data } = (await this.client.gateways.public.get(metadataHash)) as any - - const metadata = ((): RFOXMetadata => { - if (isMetadata(data)) return data - - return { - epoch: data.epoch, - epochStartTimestamp: data.epochStartTimestamp, - epochEndTimestamp: data.epochEndTimestamp, - treasuryAddress: data.treasuryAddress, - burnRate: data.burnRate, - distributionRateByStakingContract: { - [ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS_FOX]: data.distributionRate, - }, - ipfsHashByEpoch: {}, - } - })() + const { data: metadata } = (await this.client.gateways.public.get(metadataHash)) as any - for (const [epochNum, epochHash] of Object.entries(data.ipfsHashByEpoch)) { - const { data } = (await this.client.gateways.public.get(metadataHash)) as any + if (!isMetadata(metadata)) { + console.log('invalid metadata', metadata) + if (!('distributionRateByStakingContract' in metadata)) return this.migrate_v1(metadata) + } - if (isEpoch(data)) { - metadata.ipfsHashByEpoch[epochNum] = epochHash as string - continue - } + for (const [_, epochHash] of Object.entries(metadata.ipfsHashByEpoch).sort( + ([a], [b]) => Number(b) - Number(a), + )) { + const { data: epoch } = (await this.client.gateways.public.get(epochHash)) as any - const epoch: Epoch = { - number: data.number, - startTimestamp: data.startTimestamp, - endTimestamp: data.endTimestamp, - startBlock: data.startBlock, - endBlock: data.endBlock, - treasuryAddress: data.treasuryAddress, - totalRevenue: data.totalRevenue, - burnRate: data.burnRate, - ...(data.runePriceUsd && { - runePriceUsd: data.runePriceUsd, - }), - distributionStatus: data.distributionStatus, - detailsByStakingContract: { - [ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS_FOX]: { - totalRewardUnits: data.totalRewardUnits, - distributionRate: data.distributionRate, - ...(data.assetPriceUsd && { - assetPriceUsd: data.assetPriceUsd, - }), - distributionsByStakingAddress: data.distributionsByStakingAddress, - }, - }, + if (!isEpoch(epoch)) { + if (!('distributionTimestamp' in metadata)) return this.migrate_v2(metadata) } - - metadata.ipfsHashByEpoch[epochNum] = await this.addEpoch(epoch) } return metadata } catch (err) { if (isAxiosError(err)) { error( - `Failed to get content of IPFS hash (${metadataHash}): ${err.request?.data || err.response?.data || err.message}, exiting.`, + `Failed to migrate IPFS hash (${metadataHash}): ${err.request?.data || err.response?.data || err.message}, exiting.`, ) } else { - error(`Failed to get content of IPFS hash (${metadataHash}): ${err}, exiting.`) + error(`Failed to migrate IPFS hash (${metadataHash}): ${err}, exiting.`) } process.exit(1) diff --git a/cli/src/types.ts b/cli/src/types.ts index 06e7855..6d925e4 100644 --- a/cli/src/types.ts +++ b/cli/src/types.ts @@ -72,6 +72,8 @@ export type Epoch = { startTimestamp: number /** The end timestamp for this epoch */ endTimestamp: number + /** The timestamp of the pending or complete reward distribution */ + distributionTimestamp: number /** The start block for this epoch */ startBlock: number /** The end block for this epoch */