import { Account , AccountChangeAction } from 'shared/model/account' ; import config from 'shared/config' ; import BlockRepo from 'core/repository/block' ; import { IAirdropAsset , IAssetStake , IAssetVote , Stake , Transaction , TransactionModel , TransactionType , } from 'shared/model/transaction' ; import AccountRepo from 'core/repository/account' ; import ReferredUsersRepo , { ReferredUserFactor } from 'core/repository/referredUsers' ; import { ResponseEntity } from 'shared/model/response' ; import { TOTAL_PERCENTAGE } from 'core/util/const' ; import { isEqualMaps } from 'core/util/common' ; import { Address } from 'shared/model/types' ; import { FactorAction } from 'core/repository/referredUsers/interfaces' ; class StakeReward { private readonly milestones = config . CONSTANTS . FROZE . REWARDS . MILESTONES ; private readonly distance = Math . floor ( config . CONSTANTS . FROZE . REWARDS . DISTANCE ) ; calcMilestone ( height ) { const location = Math . trunc ( ( height ) / this . distance ) ; const lastMile = this . milestones [ this . milestones . length - 1 ] ; if ( location > ( this . milestones . length - 1 ) ) { return this . milestones . lastIndexOf ( lastMile ) ; } return location ; } calcReward ( height ) { return this . milestones [ this . calcMilestone ( height ) ] ; } } const stakeReward = new StakeReward ( ) ; export const calculateTotalRewardAndUnstake = ( trs , sender , isDownVote ) : { reward : number ; unstake : number ; } => { let reward = 0 ; let unstakeAmount = 0 ; if ( isDownVote ) { return { reward , unstake : unstakeAmount } ; } sender . stakes . filter ( stake => stake . isActive && trs . createdAt >= stake . nextVoteMilestone ) . forEach ( ( stake ) => { if ( stake . voteCount > 0 && ( stake . voteCount + 1 ) % config . CONSTANTS . FROZE . REWARD_VOTE_COUNT === 0 ) { const blockHeight = BlockRepo . getLastBlock ( ) . height ; const stakeRewardPercent = stakeReward . calcReward ( blockHeight ) ; reward += Math . ceil ( ( stake . amount * stakeRewardPercent ) / TOTAL_PERCENTAGE ) ; } if ( stake . voteCount + 1 === config . CONSTANTS . FROZE . UNSTAKE_VOTE_COUNT ) { unstakeAmount += stake . amount ; } } ) ; return { reward , unstake : unstakeAmount } ; } ; export const getAirdropReward = ( sender , amount , transactionType ) : IAirdropAsset => { const result = { sponsors : new Map < Address , number > ( ) , } ; if ( ! amount ) { return result ; } const availableAirdropBalance = AccountRepo . getByAddress ( config . CONSTANTS . AIRDROP . ADDRESS ) . actualBalance ; if ( ! sender || ! sender . referrals || ( sender . referrals . length === 0 ) ) { return result ; } const referrals = [ ] ; if ( transactionType === TransactionType . STAKE ) { referrals . push ( sender . referrals [ 0 ] ) ; } else { referrals . push ( ... sender . referrals ) ; } let airdropRewardAmount = 0 ; const sponsors = new Map < Address , number > ( ) ; referrals . forEach ( ( sponsor , i ) => { const reward = transactionType === TransactionType . STAKE ? Math . ceil ( ( amount * config . CONSTANTS . AIRDROP . STAKE_REWARD_PERCENT ) / TOTAL_PERCENTAGE ) : Math . ceil ( ( ( config . CONSTANTS . AIRDROP . REFERRAL_PERCENT_PER_LEVEL [ i ] ) * amount ) / TOTAL_PERCENTAGE ) ; sponsors . set ( sponsor . address , reward ) ; airdropRewardAmount += reward ; } ) ; if ( availableAirdropBalance < airdropRewardAmount ) { return result ; } result . sponsors = sponsors ; return result ; } ; export const verifyAirdrop = ( trs , amount , sender ) : ResponseEntity < void > => { const airdropReward = getAirdropReward ( sender , amount , trs . type ) ; if ( ! isEqualMaps ( airdropReward . sponsors , trs . asset . airdropReward . sponsors ) ) { return new ResponseEntity < void > ( { errors : [ `Verify failed: ${ trs . type === TransactionType . STAKE ? 'stake' : 'vote' } ` + `airdrop reward is corrupted, ` + `expected: ${ JSON . stringify ( [ ... trs . asset . airdropReward . sponsors ] ) }, ` + `actual: ${ JSON . stringify ( [ ... airdropReward . sponsors ] ) }` ] } ) ; } return new ResponseEntity < void > ( ) ; } ; export function applyFrozeOrdersRewardAndUnstake ( trs , activeOrders ) { applyRewards ( trs ) ; applyUnstake ( activeOrders , trs ) ; } function applyRewards ( trs ) { AccountRepo . updateBalanceByAddress ( trs . senderAddress , trs . asset . reward ) ; AccountRepo . updateBalanceByAddress ( config . CONSTANTS . TOTAL_SUPPLY . ADDRESS , - trs . asset . reward ) ; } function applyUnstake ( orders , trs ) { const readyToUnstakeOrders = orders . filter ( o => o . voteCount === config . CONSTANTS . FROZE . UNSTAKE_VOTE_COUNT ) ; readyToUnstakeOrders . map ( ( order ) => { order . isActive = false ; } ) ; AccountRepo . updateBalanceByAddress ( trs . senderAddress , trs . asset . unstake ) ; ReferredUsersRepo . updateStakeAmountFactor ( trs . senderAddress , trs . asset . unstake , FactorAction . SUBTRACT ) ; } export function isSponsorsExist ( trs ) { return trs . asset . airdropReward . sponsors . size !== 0 ; } export function sendAirdropReward ( trs ) { const transactionAirdropReward = trs . asset . airdropReward ; for ( const [ sponsorAddress , rewardAmount ] of transactionAirdropReward . sponsors ) { if ( rewardAmount === 0 ) { continue ; } const recipient = AccountRepo . getByAddress ( sponsorAddress ) ; recipient . actualBalance += rewardAmount ; recipient . addHistory ( AccountChangeAction . AIRDROP_REWARD_RECEIVE , trs . id ) ; AccountRepo . updateBalanceByAddress ( config . CONSTANTS . AIRDROP . ADDRESS , - rewardAmount ) ; } ReferredUsersRepo . updateRewardFactor ( trs ) ; } export function undoAirdropReward ( trs ) { const transactionAirdropReward = trs . asset . airdropReward ; for ( const [ sponsorAddress , rewardAmount ] of transactionAirdropReward . sponsors ) { if ( rewardAmount === 0 ) { continue ; } const recipient = AccountRepo . getByAddress ( sponsorAddress ) ; recipient . actualBalance -= rewardAmount ; recipient . addHistory ( AccountChangeAction . AIRDROP_REWARD_RECEIVE_UNDO , trs . id ) ; AccountRepo . updateBalanceByAddress ( config . CONSTANTS . AIRDROP . ADDRESS , rewardAmount ) ; } ReferredUsersRepo . updateRewardFactor ( trs , ReferredUserFactor . ACTION . SUBTRACT ) ; } export function undoFrozeOrdersRewardAndUnstake ( trs , sender , senderOnly ) { const updatedOrders = sender . stakes . filter ( ( order ) => { return order . nextVoteMilestone === trs . createdAt + config . CONSTANTS . FROZE . VOTE_MILESTONE ; } ) ; undoUnstake ( updatedOrders , trs , sender ) ; undoRewards ( trs , sender , senderOnly ) ; } function undoUnstake ( orders , trs , sender ) { const unstakedOrders = orders . filter ( order => ! order . isActive ) ; unstakedOrders . map ( ( order ) => { order . isActive = true ; } ) ; sender . actualBalance -= trs . asset . unstake ; ReferredUsersRepo . updateStakeAmountFactor ( sender . address , trs . asset . unstake , FactorAction . ADD ) ; } function undoRewards ( trs , sender , senderOnly ) { sender . actualBalance -= trs . asset . reward ; if ( ! senderOnly ) { AccountRepo . updateBalanceByAddress ( config . CONSTANTS . TOTAL_SUPPLY . ADDRESS , trs . asset . reward ) ; } }