diff --git a/.env.example b/.env.example index d8ac009..88a0e13 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ DATABASE_URL=postgresql://:@localhost:5432/ TOKENTERMINAL_API_KEY= LIVEPEER_COM_API_KEY= -POCKET_INFLUX_TOKEN= +CMC_API_KEY= +POKTSCAN_API_KEY= NEXT_PUBLIC_SUBGRAPH= \ No newline at end of file diff --git a/cmd/pocket.ts b/cmd/pocket.ts index 32f9467..f840e0d 100644 --- a/cmd/pocket.ts +++ b/cmd/pocket.ts @@ -1,13 +1,13 @@ import prisma from "../lib/prisma"; -// POKT Price API const axios = require("axios"); -const priceEndpoint = - "http://ec2-35-177-209-25.eu-west-2.compute.amazonaws.com/prices/pokt"; -const poktNetworkDataEndpoint = "https://poktscan.com/api/pokt-network/summary"; +const cmcAPIKey = process.env.CMC_API_KEY; +const poktscanAPIKey = process.env.POKTSCAN_API_KEY; -// .01 relays/pokt * 89% validator allocation -const relayToPOKTRatio = 0.01 * 0.89; +// POKT Pricing & Network Data Endpoints +const cmcAPIEndpoint = + "https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/historical"; +const poktscanAPIEndpoint = "https://api.poktscan.com/poktscan/api/graphql"; const coin = { name: "pocket", @@ -17,10 +17,7 @@ const coin = { // Update Pocket Network daily revenue data // a cron job should hit this endpoint every hour const pocketImport = async () => { - // This will fetch successfuly relays on the network for blockchains with revenue - // and sum up the fees for the day, on an hourly basis; totalling to the day's revenue. let revenue = 0; - let successfulRelays = 0; const project = await getProject(coin.name); @@ -49,48 +46,20 @@ const pocketImport = async () => { const toDate = new Date(); const days = dateRangeToList(fromDate, toDate); - const dateDiff = dateDiffInDays(fromDate, toDate); - const pocketPrices = await getPOKTDayPrices( - formatDate(fromDate), - formatDate(toDate) - ); - - let timeUnitMsg = "on day"; + const pocketPrices = await getPOKTDayPrices(fromDate, toDate); for (const day of days) { - const dayISO = formatDate(day); // YYYY-MM-DD + const dayISO = formatDate(day); // YYYY-MM-DDTHH:mm:ss.SSSZ const dateUnixTimestamp = day.getTime() / 1000; - const { totalAppStakes, totalPOKTsupply, totalRelays1d, totalRelays1hr } = - await getPOKTNetworkData(day); - - if (dateDiff >= 1) { - // If data was last updated was more than a day ago, - // we need to fetch all relays for the past days. - successfulRelays = totalRelays1d; - } else { - // If data was last updated less than a day ago, - // we will only update with data from the past hour. - successfulRelays = totalRelays1hr; - timeUnitMsg = "in the last hour of day"; - } - - console.log( - `Successful relays ${timeUnitMsg} ${dayISO}: ${numberWithCommas( - successfulRelays - )}.` - ); + const { totalBurned } = await getPOKTNetworkData(day); const { price: currentDayPrice } = pocketPrices.find( (x) => x.date === dayISO ); - if (successfulRelays > 0 && currentDayPrice > 0) { - revenue = - (totalAppStakes / totalPOKTsupply) * - (successfulRelays * relayToPOKTRatio * currentDayPrice); - } + revenue = totalBurned * currentDayPrice; console.log( `${project.name} estimated revenue on ${dayISO}: ${revenue.toLocaleString( @@ -143,8 +112,8 @@ const getProject = async (name: string) => { const storeDBData = async ( dayData: { - date: any; - fees: any; + date: number; + fees: number; }, projectId: number ) => { @@ -156,14 +125,12 @@ const storeDBData = async ( }); if (day != null) { - const accruedRevenue = day.revenue + dayData.fees; - await prisma.day.update({ where: { id: day.id, }, data: { - revenue: accruedRevenue, + revenue: dayData.fees, }, }); } else { @@ -189,22 +156,49 @@ const storeDBData = async ( return; }; -const getPOKTDayPrices = async (dateFrom: string, dateTo: string) => { +const getPOKTDayPrices = async (dateFrom: Date, dateTo: Date) => { const dayPrices: DayPrice[] = []; try { - const { data: response } = await axios.get( - `${priceEndpoint}?date_from=${dateFrom}&date_to=${dateTo}` + const dateFromISO = formatDate(dateFrom); + const dateToISO = formatDate(dateTo); + + const { data: response }: { data: Response } = await axios.get( + `${cmcAPIEndpoint}?symbol=POKT&time_start=${dateFromISO}&time_end=${dateToISO}`, + { + headers: { + "Content-Type": "application/json", + "X-CMC_PRO_API_KEY": cmcAPIKey, + }, + } ); if (!response) { throw new Error("No data returned by the price API."); } - for (const entry of response.data) { - const price = parseFloat(entry.price); - const date = String(entry.created_date); + const uniqueDates = new Set(); + const dateQuotes: { [date: string]: number[] } = {}; + + response.data.quotes.forEach((quote) => { + const date = quote.timestamp.slice(0, 10); + uniqueDates.add(date); + if (!dateQuotes[date]) { + dateQuotes[date] = []; + } + dateQuotes[date].push(quote.quote.USD.price); + }); + + const averagePrices: { [date: string]: number } = {}; + + Array.from(uniqueDates).forEach((date) => { + const prices = dateQuotes[date]; + const sum = prices.reduce((acc, price) => acc + price, 0); + const average = sum / prices.length; + averagePrices[date] = average; + }); - dayPrices.push({ price, date } as DayPrice); + for (const [date, price] of Object.entries(averagePrices)) { + dayPrices.push({ date, price }); } return dayPrices; @@ -213,108 +207,78 @@ const getPOKTDayPrices = async (dateFrom: string, dateTo: string) => { } }; -const getPOKTNetworkData = async (date: Date) => { - const dateFrom = date; - const ISODateFrom = formatDate(dateFrom); +const query = ` + query ($pagination: ListInput!) { + ListPoktTransaction(pagination: $pagination) { + items { + amount + block_time + result_code + } + } + } +`; - const dateTo = new Date(dateFrom.setUTCDate(dateFrom.getUTCDate() + 1)); - const ISODateTo = formatDate(dateTo); +const getPOKTNetworkData = async (date: Date) => { + const ISODate = formatDate(date); // TODO: Make use of hourly data instead of days - const payload = { from: ISODateFrom, to: ISODateTo, debug: true }; + const variables = { + pagination: { + limit: 10, + filter: { + operator: "AND", + properties: [ + { + operator: "EQ", + type: "STRING", + property: "type", + value: "dao_tranfer", + }, + { + operator: "EQ", + type: "STRING", + property: "action", + value: "dao_burn", + }, + { + operator: "GTE", + type: "DATE", + property: "block_time", + value: ISODate, + }, + ], + }, + }, + }; try { - const { data: response } = await axios.post( - poktNetworkDataEndpoint, - payload - ); - console.log(payload); - if (!response || !response.length) { - throw new Error("No data returned by the poktscan API."); - } - - const [data] = response; - console.log(data); - const blocks = data?.blocks as BlockData[]; - - const latestBlock: BlockData = filterLastBlock(blocks); - const lastFourBlocks: BlockData[] = filterLastFourBlocks(blocks); - - const totalRelays1d = data.total_relays_completed; - const totalRelays1hr = lastFourBlocks.reduce( - (sum, block) => sum + block.total_relays_completed, - 0 - ); - if(!blocks.length) { - return { - totalAppStakes: 0, - totalPOKTsupply: 0, - totalRelays1d: 0, - totalRelays1hr: 0, - } + const response: PoktScanResponse = axios.post(poktscanAPIEndpoint, { + query, + variables, + headers: { + "Content-Type": "application/json", + Authorization: poktscanAPIKey, + }, + }); + + if (!response || !response.data) { + throw new Error("No data returned by the PoktScan API."); } + + // This is the total burned for a single day + const totalBurned = response.data.ListPoktTransaction.items + .filter((burnTx) => burnTx.result_code === 0) + .reduce((sum, burnTx) => sum + burnTx.amount, 0); + return { - totalAppStakes: latestBlock.apps_staked_tokens, - totalPOKTsupply: latestBlock.total_supply, - totalRelays1d, - totalRelays1hr, + totalBurned, }; } catch (e) { throw new Error(e); } }; -const filterLastBlock = (blocksData: BlockData[]) => { - let maxid = 0; - let maxObj: BlockData; - - blocksData.forEach(function (obj: BlockData) { - if (obj.height > maxid) { - maxObj = obj; - maxid = maxObj.height; - } - }); - - return maxObj as BlockData; -}; - -const filterLastFourBlocks = (blocksData: BlockData[]) => { - let blockNumbers = []; - const latestBlocks = []; - - blocksData.forEach(function (obj: BlockData) { - blockNumbers.push(obj.height); - }); - - blockNumbers = blockNumbers - .sort((a, b) => (a < b ? 1 : a > b ? -1 : 0)) - .slice(0, 4); - - blocksData.forEach(function (obj: BlockData) { - if (blockNumbers.includes(obj.height)) { - latestBlocks.push(obj); - } - }); - - return latestBlocks; -}; - -const dateDiffInDays = (fromDate: Date, toDate: Date): number => { - const _MS_PER_DAY = 1000 * 60 * 60 * 24; - const utc1 = Date.UTC( - fromDate.getUTCFullYear(), - fromDate.getUTCMonth(), - fromDate.getUTCDate() - ); - const utc2 = Date.UTC( - toDate.getUTCFullYear(), - toDate.getUTCMonth(), - toDate.getUTCDate() - ); - - return Math.floor((utc2 - utc1) / _MS_PER_DAY); -}; - const dateRangeToList = (fromDate: Date, toDate: Date): Date[] => { const dayList: Date[] = []; @@ -330,24 +294,61 @@ const dateRangeToList = (fromDate: Date, toDate: Date): Date[] => { }; const formatDate = (date: Date) => { - return date.toISOString().slice(0, 10); + return date.toISOString(); }; -function numberWithCommas(x) { - return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); -} - type DayPrice = { date: string; price: number; }; -type BlockData = { - time: string; - height: number; - total_supply: number; - apps_staked_tokens: number; - total_relays_completed: number; +type Quote = { + timestamp: string; + quote: { + USD: { + price: number; + volume_24h: number; + market_cap: number; + total_supply: number; + circulating_supply: number; + timestamp: string; + }; + }; +}; + +type Response = { + status: { + timestamp: string; + error_code: number; + error_message: null | string; + elapsed: number; + credit_count: number; + notice: null | string; + }; + data: { + quotes: Quote[]; + id: number; + name: string; + symbol: string; + is_active: number; + is_fiat: number; + }; +}; + +type PoktScanTransaction = { + amount: number; + block_time: string; + result_code: number; +}; + +type PoktScanTransactionList = { + items: PoktScanTransaction[]; +}; + +type PoktScanResponse = { + data: { + ListPoktTransaction: PoktScanTransactionList; + }; }; pocketImport()