diff --git a/js/package.json b/js/package.json index 7c1e06f..709c976 100644 --- a/js/package.json +++ b/js/package.json @@ -31,7 +31,7 @@ }, "scripts": { "client": "PORT=3001 FAST_REFRESH=false react-scripts start", - "server": "node -r dotenv/config --trace-warnings --max-old-space-size=13958 server 2>&1 | tee /tmp/cryptoecon.log", + "server": "node -r dotenv/config --trace-warnings --max-old-space-size=33958 server 2>&1 | tee /tmp/cryptoecon.log", "dev": "tsnd -r dotenv/config --max-old-space-size=16384 ./server", "test-process": "node -r dotenv/config --trace-warnings --max-old-space-size=16384 ./server/worker/process.childprocess.test.js", "refresh-lm-snapshot": "source .env && curl -H 'Content-Type: application/json' -H \"$HEADER_SECRET\" -X POST -d '{\"query\": \"query GetSnapshot { snapshots_new(limit: 1 order_by: {id: desc}) { snapshot_data } }\"}' $SNAPSHOT_URL -o ./snapshots/snapshot_lm_latest.trash.json", diff --git a/js/server/config.js b/js/server/config.js index cdbf68a..d8f101a 100644 --- a/js/server/config.js +++ b/js/server/config.js @@ -1,6 +1,7 @@ function createConfig({ startsAt, durationInWeeks, + weeksToTotalMaturity, intervalDurationMinutes, initialRowan, initialRewardMultiplier, @@ -58,7 +59,9 @@ function createConfig({ DEPOSITS_ALLOWED_DURATION_MS, MULTIPLIER_MATURITY: REWARD_ACCRUAL_DURATION_MS / 1000 / 60 / EVENT_INTERVAL_MINUTES, // 6 weeks in in 200minute intervals, - NUMBER_OF_INTERVALS_TO_RUN: REWARD_ACCRUAL_DURATION_INTERVAL_COUNT * 2, // duration of bucket drain + duration to latest possible multiplier maturity + NUMBER_OF_INTERVALS_TO_RUN: + REWARD_ACCRUAL_DURATION_INTERVAL_COUNT * + (weeksToTotalMaturity / durationInWeeks), // duration of bucket drain + duration to latest possible multiplier maturity REWARD_ACCRUAL_DURATION_INTERVAL_COUNT, INITIAL_REWARD_MULTIPLIER: initialRewardMultiplier, }; @@ -70,6 +73,7 @@ module.exports = { initialRowan: 10_000_000, startsAt: '2021-08-24T20:06:15.000Z', durationInWeeks: 6, + weeksToTotalMaturity: 12, intervalDurationMinutes: 200, initialRewardMultiplier: 0.25, }), @@ -77,6 +81,7 @@ module.exports = { initialRowan: 40_000_000, startsAt: '2021-10-04T00:00:00.000Z', durationInWeeks: 6, + weeksToTotalMaturity: 6.1, intervalDurationMinutes: 59, initialRewardMultiplier: 1, }), @@ -84,6 +89,15 @@ module.exports = { initialRowan: 1_000_000, startsAt: '2021-10-05T19:00:00.000Z', durationInWeeks: 2, + weeksToTotalMaturity: 2.1, + intervalDurationMinutes: 60, + initialRewardMultiplier: 1, + }), + bonus_v1_ixo: createConfig({ + initialRowan: 500_000, + startsAt: '2021-10-12T13:29:01.255Z', + durationInWeeks: 2, + weeksToTotalMaturity: 2.1, intervalDurationMinutes: 60, initialRewardMultiplier: 1, }), diff --git a/js/server/core/load/loadLiquidityMinersSnapshot.js b/js/server/core/load/loadLiquidityMinersSnapshot.js index 2790dbb..49b5843 100644 --- a/js/server/core/load/loadLiquidityMinersSnapshot.js +++ b/js/server/core/load/loadLiquidityMinersSnapshot.js @@ -2,6 +2,7 @@ const { TESTNET } = require('../../constants/snapshot-source-names'); const slonik = require('slonik'); const { getDatabase } = require('./utils/getDatabase'); +const lmHarvestStartingState = require('./lm-harvest-starting-state.json'); /* WARNING: DO NOT ADD MORE QUERIES OR FIELDS TO THE GRAPHQL QUERY. QUERIES ARE CACHED USING THE LENGTH OF THE TEXT CONTENT OF THE RESPONSE OBJECT @@ -88,6 +89,18 @@ const getSQLQueryByNetwork = (network, rewardProgram) => { for (let snapshot of snapshots_new) { const liquidityByTokens = snapshot.snapshot_data[snapshot.address]; for (let token in liquidityByTokens) { + let startingUserState = 0; + const startingPoolState = lmHarvestStartingState[token]; + if ( + !startingPoolState && + Object.values(liquidityByTokens).filter((v) => v.length > 0) + .length > 2 + ) { + debugger; + } + if (startingPoolState) { + startingUserState = startingPoolState[snapshot.address] || 0; + } let liquidityDeltaEvents = liquidityByTokens[token]; let total = 0; for (let deltaEvent of liquidityDeltaEvents) { diff --git a/js/server/core/process/augmentVSData.js b/js/server/core/process/augmentVSData.js index e477822..b9499b8 100644 --- a/js/server/core/process/augmentVSData.js +++ b/js/server/core/process/augmentVSData.js @@ -46,7 +46,9 @@ exports.augmentVSData = ( const userAtTimestamp = timestamp.users[user.address] || new User(); if (userAtTimestamp.totalAccruedCommissionsAndClaimableRewards) { accum[user.address] = - userAtTimestamp.totalAccruedCommissionsAndClaimableRewards; + userAtTimestamp.totalAccruedCommissionsAndClaimableRewards + + userAtTimestamp.dispensed + + userAtTimestamp.claimedCommissionsAndRewardsAwaitingDispensation; } return accum; }, {}); diff --git a/js/server/core/transform/lm-util.js b/js/server/core/transform/lm-util.js index 0396c3c..7a4c8a2 100644 --- a/js/server/core/transform/lm-util.js +++ b/js/server/core/transform/lm-util.js @@ -4,7 +4,9 @@ const { getTimeIndex } = require('../../util/getTimeIndex'); const { DelegateEvent } = require('../types'); const moment = require('moment'); const fetch = require('cross-fetch').fetch; +const lmHarvestStartingState = require('../load/lm-harvest-starting-state.json'); +global.ACCOUNT_FOR_INITIAL_POOL_STATE = true; const getLMTimeseriesFinalIndex = (snapshotData) => { // Snapshot timeseries (generated by Vanir) overshoots by 1 extra interval. Account for this. const finalIndex = [ @@ -50,6 +52,8 @@ function remapLMAddresses(addresses, deltaCoeff, rewardProgram) { EVENT_INTERVAL_MINUTES, amount: interval.delta, //* deltaCoeff, delegateAddress: address, + token: token, + rawTimestamp: interval.unix_timestamp * 1000, }); }) .filter((e) => e.amount !== 0); @@ -57,7 +61,105 @@ function remapLMAddresses(addresses, deltaCoeff, rewardProgram) { return addressTokenEvents; }); - const rawEvents = _.flattenDeep(mapped); + let rawEvents = _.flattenDeep(mapped); + + if (global.ACCOUNT_FOR_INITIAL_POOL_STATE) { + rawEvents = rawEvents + .sort((a, b) => a.amount - b.amount) + .sort((a, b) => a.rawTimestamp - b.rawTimestamp); + let rawTimestampGroupedEvents = _.groupBy(rawEvents, 'rawTimestamp'); + _.mapValues(rawTimestampGroupedEvents, (timeIntervalEvents) => { + return timeIntervalEvents.map((event) => { + if (event.amount < 0 && rewardProgram === 'harvest') { + const subtractMaxFromUserPool = (token) => { + // initial token balances for all users + const tokenBalances = lmHarvestStartingState[token]; + // initial token balance for current user + const remainingInitialRemovableBalance = tokenBalances + ? tokenBalances[event.delegateAddress] || 0 + : 0; + // normalized initial token balance for user + const convertedVal = +remainingInitialRemovableBalance / 10 ** 18; + // subtract (add the negative) the withdrawal amount from the initial amount + let amountRemainingAfterRemoval = convertedVal + event.amount; + // if the result is negative, the withdrawal exceeded the initial amount + if (amountRemainingAfterRemoval < 0) { + // if the token existed at the starting time + if (lmHarvestStartingState[token]) + // the initial amount has now been zero'ed out + lmHarvestStartingState[token][event.delegateAddress] = 0; + // the remaining withdrawal = the amount that it exceeded the initial amount + event.amount = amountRemainingAfterRemoval; + } else { + // if the token existed at program genesis + if (lmHarvestStartingState[token]) + // if there is a positive amount remaining, set to that positive amount. + lmHarvestStartingState[token][event.delegateAddress] = BigInt( + Math.floor(amountRemainingAfterRemoval * 10 ** 18) + ).toString(); + // the withdrawal is covered by the initial amount + event.amount = 0; + } + }; + // if the event doesn't have a pool + if (event.token === 'rowan') { + // guess that the pool is the first non-rowan token that this user also removed + const externalAssetEvents = + timeIntervalEvents.length > 1 + ? timeIntervalEvents.filter( + (ev) => + ev.token !== 'rowan' && + ev.amount < 0 && + ev.delegateAddress === event.delegateAddress + ) + : []; + // if there is a non-rowan withdrawal + if (externalAssetEvents.length) { + // subtract this rowan amount from those + while (event.amount < 0 && externalAssetEvents.length) { + if ( + event.delegateAddress === + 'sif1vcd6gx3jqng2m7tw6k7e766skayv5rkhn3levl' + ) { + // debugger; + } + subtractMaxFromUserPool(externalAssetEvents.pop().token); + } + } else { + for (let tokenKey in lmHarvestStartingState) { + if ( + lmHarvestStartingState[tokenKey][event.delegateAddress] > 0 + ) { + subtractMaxFromUserPool(tokenKey); + } + if ( + event.delegateAddress === + 'sif1vcd6gx3jqng2m7tw6k7e766skayv5rkhn3levl' + ) { + // debugger; + } + if (event.amount === 0) { + break; + } + } + } + } else { + subtractMaxFromUserPool(event.token); + } + } + }); + }); + + const regendudeinitial = Object.entries(lmHarvestStartingState) + .map(([k, v]) => { + return v['sif1vcd6gx3jqng2m7tw6k7e766skayv5rkhn3levl']; + }) + .filter((v) => !!v); + const regendude = rawEvents.filter( + (ev) => + ev.delegateAddress === 'sif1vcd6gx3jqng2m7tw6k7e766skayv5rkhn3levl' + ); + } let allTimeIntervalEvents = _.groupBy(rawEvents, 'timestamp'); allTimeIntervalEvents = _.mapValues( allTimeIntervalEvents, @@ -82,12 +184,21 @@ function remapLMAddresses(addresses, deltaCoeff, rewardProgram) { return _.mapValues( timeIntervalAddressEvents, (addressEvents, address) => { + if (global.ACCOUNT_FOR_INITIAL_POOL_STATE) { + addressEvents = addressEvents.sort((a, b) => { + return a.rawTimestamp - b.rawTimestamp; + }); + } return [ DelegateEvent.fromJSON({ timestamp: parseInt(timeInterval), delegateAddress: address, amount: addressEvents.reduce((accum, addressEvent) => { - return accum + parseFloat(addressEvent.amount); + const nextVal = accum + parseFloat(addressEvent.amount); + if (global.ACCOUNT_FOR_INITIAL_POOL_STATE) { + return Math.max(0, nextVal); + } + return nextVal; }, 0), }), ]; diff --git a/js/server/core/types/DelegateEvent.js b/js/server/core/types/DelegateEvent.js index 2bb2322..eaf4f2e 100644 --- a/js/server/core/types/DelegateEvent.js +++ b/js/server/core/types/DelegateEvent.js @@ -1,26 +1,28 @@ class DelegateEvent { - constructor () { + constructor() { this.timestamp = 0; this.delegateAddress = null; this.validatorRewardAddress = null; this.validatorStakeAddress = null; this.commission = 0; this.amount = 0; + this.token = ''; + this.rawTimestamp = 0; } - clearTimestamp () { + clearTimestamp() { this.timestamp = undefined; } - static fromJSON (props) { + static fromJSON(props) { return Object.assign(new this(), props); } - cloneWith (props) { + cloneWith(props) { return Object.assign(Object.assign(new DelegateEvent(), this), props); } } module.exports = { - DelegateEvent + DelegateEvent, }; diff --git a/js/server/main.js b/js/server/main.js index 94a1d55..48a6b6e 100644 --- a/js/server/main.js +++ b/js/server/main.js @@ -214,8 +214,19 @@ const server = new ApolloServer({ distributionPattern: 'LINEAR', }, { - displayName: `Sif's Bonus Pool`, - description: `Immediately earn and claim rewards by pooling JUNØ.`, + displayName: `Sif's IXO Bonus Pool`, + description: `Earn Rowan every 60 minutes and claim rewards immediately by pooling IXO.`, + rewardProgramType: 'lm', + rewardProgramName: 'bonus_v1_ixo', + incentivizedPoolSymbols: ['ixo'], + documentationURL: + 'https://docs.sifchain.finance/resources/rewards-programs', + isUniversal: false, + distributionPattern: 'LINEAR', + }, + { + displayName: `Sif's Junø Bonus Pool`, + description: `Earn Rowan every 60 minutes and claim rewards immediately by pooling JUNØ.`, rewardProgramType: 'lm', rewardProgramName: 'bonus_v1', incentivizedPoolSymbols: ['juno'], diff --git a/js/server/worker/process.childprocess.test.js b/js/server/worker/process.childprocess.test.js index 029b752..bb64f11 100644 --- a/js/server/worker/process.childprocess.test.js +++ b/js/server/worker/process.childprocess.test.js @@ -2,6 +2,7 @@ const { BackgroundProcessor } = require('./process.childprocess'); const _ = require('lodash'); const { MAINNET } = require('../constants/snapshot-source-names'); const { GET_LM_CURRENT_APY_SUMMARY } = require('../constants/action-names'); +const { getTimeIndex } = require('../util/getTimeIndex'); // simple test setup const describe = async (description, describer) => { @@ -39,11 +40,29 @@ const describe = async (description, describer) => { console.groupEnd(); }; -const runTests = (type, parsedData, network) => { +const runTests = (type, parsedData, network, programName) => { const finalGlobalTimestampState = parsedData.processedData[parsedData.processedData.length - 1]; const users = Object.values(finalGlobalTimestampState.users); + const totalValuePerUser = Object.entries( + parsedData.processedData[getTimeIndex('now', programName)].users + ).reduce((prev, [addr, curr]) => { + if (!curr) return prev; + prev[addr] = + curr.totalAccruedCommissionsAndClaimableRewards + + curr.claimedCommissionsAndRewardsAwaitingDispensation + + curr.forfeitedCommissions + + curr.forfeited + + curr.dispensed; + return prev; + }, {}); + + require('fs').writeFileSync( + './userExitStates.new.json', + Buffer.from(JSON.stringify(totalValuePerUser, null, 2)) + ); + const totalPoolDominanceRatio = _.sum( _.flattenDeep( _.map(users, (u) => u.tickets.map((t) => t.poolDominanceRatio)) @@ -88,17 +107,19 @@ const runTests = (type, parsedData, network) => { }; const bp = new BackgroundProcessor(); +// const bp2 = new BackgroundProcessor(); +const programName = 'bonus_v1_ixo'; bp.reloadAndReprocessSnapshots({ network: MAINNET, - rewardProgram: 'bonus_v1', + rewardProgram: programName, }) // test reload caching // .then(async () => bp.reloadAndReprocessSnapshots({ network: MAINNET })) .then(async () => { - await runTests('lm', bp.lmDataParsed, MAINNET); + await runTests('lm', bp.lmDataParsed, MAINNET, programName); console.log( bp.dispatch(GET_LM_CURRENT_APY_SUMMARY, { - programName: 'bonus_v1', + programName: programName, }) ); // await runTests('vs', bp.vsDataParsed, MAINNET); diff --git a/js/src/App.js b/js/src/App.js index 7dc6318..bc6d81e 100644 --- a/js/src/App.js +++ b/js/src/App.js @@ -1,8 +1,4 @@ -import { - START_DATETIME, - networks, - RECENT_ADDRESS_LIST_STORAGE_KEY, -} from './config'; +import { networks, RECENT_ADDRESS_LIST_STORAGE_KEY } from './config'; import './App.css'; import React, { useEffect, useMemo, useState } from 'react'; import { @@ -19,7 +15,10 @@ import DataChart from './DataChart'; import DataStackAll from './DataStackAll'; import { StatBlocks } from './StatBlocks'; import { UserDataSummary } from './UserDataSummary'; +import serverConfigs from './serverConfig'; +const serverConfig = + serverConfigs[window.sessionStorage.getItem('rewardProgram')]; // show all fields locally const SHOULD_HIDE_NON_USER_FRIENDLY_FIELDS = !!process.env.REACT_APP_DEPLOYMENT_TAG; @@ -92,7 +91,7 @@ class Router { } // const now = moment.utc(Date.parse(new Date())); // function initTimestamp() { -// return moment.duration(now.diff(START_DATETIME)).asMinutes() / 200; +// return moment.duration(now.diff(START_DATETIME)).asMinutes() / serverConfig.EVENT_INTERVAL_MINUTES; // } const CountDown = ({ until = moment() }) => { @@ -191,7 +190,9 @@ class App extends React.Component { time = moment.utc(time); return ( Math.floor( - moment.duration(time.diff(moment.utc(START_DATETIME))).asMinutes() / 200 + moment + .duration(time.diff(moment.utc(serverConfig.START_DATETIME))) + .asMinutes() / serverConfig.EVENT_INTERVAL_MINUTES ) + 1 ); } @@ -314,8 +315,11 @@ class App extends React.Component { updateTimestamp(timeIndex) { // because genesis block is included - const minutes = timeIndex * 200; - const dateObj = moment.utc(START_DATETIME).add(minutes, 'm').utc(); + const minutes = timeIndex * serverConfig.EVENT_INTERVAL_MINUTES; + const dateObj = moment + .utc(serverConfig.START_DATETIME) + .add(minutes, 'm') + .utc(); const date = dateObj; this.setState({ date, @@ -605,7 +609,7 @@ class App extends React.Component { {this.state.date.format( `ddd MMMM Do YYYY[,] [${this.state.date .clone() - .subtract(200, 'minutes') + .subtract(serverConfig.EVENT_INTERVAL_MINUTES, 'minutes') .format(`hh:mm A`)}] - hh:mm A` ) + ' UTC'} @@ -622,7 +626,10 @@ class App extends React.Component { `ddd MMMM Do YYYY[,] [${this.state.date .clone() .local() - .subtract(200, 'minutes') + .subtract( + serverConfig.EVENT_INTERVAL_MINUTES, + 'minutes' + ) .format(`hh:mm A`)}] - hh:mm A` ) + ' LOCAL'}