protocol_fee_reward_state.gno

package staker

import (
	"errors"

	"gno.land/p/gnoswap/consts"
	gnsmath "gno.land/p/gnoswap/gnsmath"
	u256 "gno.land/p/gnoswap/uint256"
	"gno.land/r/gnoswap/gov/staker"
)

type ProtocolFeeRewardStateResolver struct {
	*staker.ProtocolFeeRewardState
}

func NewProtocolFeeRewardStateResolver(protocolFeeRewardState *staker.ProtocolFeeRewardState) *ProtocolFeeRewardStateResolver {
	return &ProtocolFeeRewardStateResolver{protocolFeeRewardState}
}

// IsClaimable checks if rewards can be claimed at the given timestamp.
// Rewards are claimable if the current timestamp is greater than the last claimed timestamp.
//
// Parameters:
//   - currentTimestamp: current timestamp to check against
//
// Returns:
//   - bool: true if rewards can be claimed, false otherwise
func (p *ProtocolFeeRewardStateResolver) IsClaimable(currentTimestamp int64) bool {
	return p.GetClaimedTimestamp() < currentTimestamp
}

// GetClaimableRewardAmounts calculates the claimable reward amounts for all tokens.
// This includes both accumulated rewards and newly earned rewards based on current state.
//
// Parameters:
//   - accumulatedRewardsX128PerStake: current system-wide accumulated rewards per stake for all tokens
//   - currentTimestamp: current timestamp
//
// Returns:
//   - map[string]int64: map of token path to claimable reward amount
//   - error: nil on success, error if claiming is not allowed
func (p *ProtocolFeeRewardStateResolver) GetClaimableRewardAmounts(
	accumulatedRewardsX128PerStake map[string]*u256.Uint,
	currentTimestamp int64,
) (map[string]int64, error) {
	newlyEarnedRewards, err := p.calculateClaimableRewards(accumulatedRewardsX128PerStake, currentTimestamp)
	if err != nil {
		return nil, err
	}

	claimableRewards := make(map[string]int64)
	accumulatedRewards := p.GetAccumulatedRewards()
	claimedRewards := p.GetClaimedRewards()

	for token, accumulatedReward := range accumulatedRewards {
		claimableRewards[token] = gnsmath.SafeSubInt64(accumulatedReward, claimedRewards[token])
	}

	for token, newlyEarnedReward := range newlyEarnedRewards {
		claimableRewards[token] = gnsmath.SafeAddInt64(claimableRewards[token], newlyEarnedReward)
	}

	return claimableRewards, nil
}

// calculateClaimableRewards calculates newly earned rewards for all tokens since the last update.
// This method uses the difference between current and stored reward debt to calculate earnings.
//
// Parameters:
//   - accumulatedRewardsX128PerStake: current system-wide accumulated rewards per stake for all tokens
//   - currentTimestamp: current timestamp
//
// Returns:
//   - map[string]int64: map of token path to newly earned reward amount
func (p *ProtocolFeeRewardStateResolver) calculateClaimableRewards(
	accumulatedRewardsX128PerStake map[string]*u256.Uint,
	currentTimestamp int64,
) (map[string]int64, error) {
	// Don't calculate rewards for past timestamps
	if p.GetAccumulatedTimestamp() >= currentTimestamp {
		return make(map[string]int64), nil
	}

	rewardAmounts := make(map[string]int64)
	stakedAmount := p.GetStakedAmount()

	// Calculate rewards for each token type
	for token, accumulatedRewardX128PerStake := range accumulatedRewardsX128PerStake {
		// Get reward debt for this token
		rewardDebtX128 := p.GetRewardDebtX128ForToken(token)
		if rewardDebtX128 == nil {
			rewardDebtX128 = u256.Zero()
		}

		// Calculate the difference in accumulated rewards per stake since last update
		// Using modular arithmetic for accumulator values - underflow is allowed and handled correctly
		rewardDebtDeltaX128 := u256.Zero().Sub(
			accumulatedRewardX128PerStake,
			rewardDebtX128,
		)

		// Multiply by staked amount to get total reward for this staker and token
		rewardAmount := u256.MulDiv(
			rewardDebtDeltaX128,
			u256.NewUintFromInt64(stakedAmount),
			consts.Q128(),
		)

		rewardAmounts[token] = gnsmath.SafeConvertToInt64(rewardAmount)
	}

	return rewardAmounts, nil
}

// addStake increases the staked amount for this address.
// This method should be called when a user increases their stake.
//
// Parameters:
//   - amount: amount of stake to add
func (p *ProtocolFeeRewardStateResolver) addStake(amount int64) {
	p.SetStakedAmount(gnsmath.SafeAddInt64(p.GetStakedAmount(), amount))
}

// removeStake decreases the staked amount for this address.
// This method should be called when a user decreases their stake.
//
// Parameters:
//   - amount: amount of stake to remove
func (p *ProtocolFeeRewardStateResolver) removeStake(amount int64) {
	newAmount := gnsmath.SafeSubInt64(p.GetStakedAmount(), amount)
	if newAmount < 0 {
		newAmount = 0
	}
	p.SetStakedAmount(newAmount)
}

// claimRewards processes reward claiming for all tokens and updates the claim state.
// This method validates claimability and transfers accumulated rewards to claimed status.
//
// Parameters:
//   - currentTimestamp: current timestamp
//
// Returns:
//   - map[string]int64: map of token path to claimed reward amount
//   - error: nil on success, error if reward debt is stale
func (p *ProtocolFeeRewardStateResolver) claimRewards(currentTimestamp int64) (map[string]int64, error) {
	if !p.IsClaimable(currentTimestamp) {
		return make(map[string]int64), nil
	}

	if p.GetAccumulatedTimestamp() < currentTimestamp {
		return nil, errors.New("must update reward debt before claiming rewards")
	}

	currentClaimedRewards := make(map[string]int64)
	accumulatedRewards := p.GetAccumulatedRewards()
	claimedRewards := p.GetClaimedRewards()

	// Calculate and update claimed amounts for each token
	for token, rewardAmount := range accumulatedRewards {
		claimedAmount := claimedRewards[token]
		currentClaimedRewards[token] = gnsmath.SafeSubInt64(rewardAmount, claimedAmount)
		p.SetClaimedRewardForToken(token, rewardAmount)
	}

	p.SetClaimedTimestamp(currentTimestamp)

	return currentClaimedRewards, nil
}

// updateRewardDebtX128 updates the reward debt and accumulates new rewards for all tokens.
// This method should be called before any stake changes to ensure accurate reward tracking.
//
// Parameters:
//   - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake for all tokens
//   - currentTimestamp: current timestamp
func (p *ProtocolFeeRewardStateResolver) updateRewardDebtX128(
	accumulatedProtocolFeeX128PerStake map[string]*u256.Uint,
	currentTimestamp int64,
) error {
	// Don't update if we're looking at a past timestamp
	if p.GetAccumulatedTimestamp() >= currentTimestamp {
		return nil
	}

	// Calculate and accumulate new rewards for all tokens
	rewardAmounts, err := p.calculateClaimableRewards(accumulatedProtocolFeeX128PerStake, currentTimestamp)
	if err != nil {
		return err
	}

	// Update reward debt for all tokens
	p.SetRewardDebtX128(accumulatedProtocolFeeX128PerStake)

	// Add newly calculated rewards to accumulated amounts
	accumulatedRewards := p.GetAccumulatedRewards()
	for token, rewardAmount := range rewardAmounts {
		p.SetAccumulatedRewardForToken(token, gnsmath.SafeAddInt64(accumulatedRewards[token], rewardAmount))
	}

	p.SetAccumulatedTimestamp(currentTimestamp)

	return nil
}

// addStakeWithUpdateRewardDebtX128 adds stake and updates reward debt in one operation.
// This ensures rewards are properly calculated before the stake change takes effect.
//
// Parameters:
//   - amount: amount of stake to add
//   - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake
//   - currentTimestamp: current timestamp
func (p *ProtocolFeeRewardStateResolver) addStakeWithUpdateRewardDebtX128(
	amount int64,
	accumulatedProtocolFeeX128PerStake map[string]*u256.Uint,
	currentTimestamp int64,
) error {
	err := p.updateRewardDebtX128(accumulatedProtocolFeeX128PerStake, currentTimestamp)
	if err != nil {
		return err
	}

	p.addStake(amount)

	return nil
}

// removeStakeWithUpdateRewardDebtX128 removes stake and updates reward debt in one operation.
// This ensures rewards are properly calculated before the stake change takes effect.
//
// Parameters:
//   - amount: amount of stake to remove
//   - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake
//   - currentTimestamp: current timestamp
func (p *ProtocolFeeRewardStateResolver) removeStakeWithUpdateRewardDebtX128(
	amount int64,
	accumulatedProtocolFeeX128PerStake map[string]*u256.Uint,
	currentTimestamp int64,
) error {
	err := p.updateRewardDebtX128(accumulatedProtocolFeeX128PerStake, currentTimestamp)
	if err != nil {
		return err
	}

	p.removeStake(amount)

	return nil
}

// claimRewardsWithUpdateRewardDebtX128 claims rewards and updates reward debt in one operation.
// This ensures all rewards are properly calculated before claiming.
//
// Parameters:
//   - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake
//   - currentTimestamp: current timestamp
//
// Returns:
//   - map[string]int64: map of token path to claimed reward amount
//   - error: nil on success, error if claiming fails
func (p *ProtocolFeeRewardStateResolver) claimRewardsWithUpdateRewardDebtX128(
	accumulatedProtocolFeeX128PerStake map[string]*u256.Uint,
	currentTimestamp int64,
) (map[string]int64, error) {
	p.updateRewardDebtX128(accumulatedProtocolFeeX128PerStake, currentTimestamp)

	return p.claimRewards(currentTimestamp)
}