PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/gnoswap/staker/v1

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
v1
Namespace
gnoswap / staker
Files
28 (README)(gnomod.toml)
Exported functions
14
Module
gno.land/r/gnoswap/staker/v1
gno
0.9

Files (28)

  • README.mdmarkdown
  • gnomod.tomltoml
  • assert.gnogno
  • calculate_pool_position_reward.gnogno
  • consts.gnogno
  • doc.gnogno
  • emits.gnogno
  • errors.gnogno
  • event_info.gnogno
  • external_deposit_fee.gnogno
  • external_incentive.gnogno
  • external_token_list.gnogno
  • getter_utils.gnogno
  • getter.gnogno
  • init.gnogno
  • instance.gnogno
  • manage_pool_tier_and_warmup.gnogno
  • protocol_fee_unstaking.gnogno
  • render.gnogno
  • reward_calculation_incentives.gnogno
  • reward_calculation_pool_tier.gnogno
  • reward_calculation_pool.gnogno
  • reward_calculation_tick.gnogno
  • reward_calculation_warmup.gnogno
  • staker.gnogno
  • type.gnogno
  • unstaked_position.gnogno
  • utils.gnogno
  • calculate_pool_position_reward.gnogno
    1package staker23import (4	"time"56	"gno.land/p/gnoswap/gnsmath/v1"7	ufmt "gno.land/p/nt/ufmt/v0"8

    Functions

    • NewDepositResolver(deposit *gno.land/r/gnoswap/staker.Deposit) *gno.land/r/gnoswap/staker/v1.DepositResolver

    • NewDeposits() *gno.land/r/gnoswap/staker/v1.Deposits

    • NewExternalIncentiveResolver(externalIncentive *gno.land/r/gnoswap/staker.ExternalIncentive) *gno.land/r/gnoswap/staker/v1.ExternalIncentiveResolver

    • NewExternalIncentives() *gno.land/r/gnoswap/staker/v1.ExternalIncentives

    • NewIncentivesResolver(incentives *gno.land/r/gnoswap/staker.Incentives) *gno.land/r/gnoswap/staker/v1.IncentivesResolver

    • NewPoolResolver(pool *gno.land/r/gnoswap/staker.Pool) *gno.land/r/gnoswap/staker/v1.PoolResolver

    • NewPools() *gno.land/r/gnoswap/staker/v1.Pools

    • NewPoolTier(pools *gno.land/r/gnoswap/staker/v1.Pools, currentTime int64, initialPoolPath string, getEmission func() (int64, .uverse.error), getHalvingBlocksInRange func(int64, int64) ([]int64, []int64, .uverse.error)) *gno.land/r/gnoswap/staker/v1.PoolTier

    • NewPoolTierBy(membership *gno.land/p/nt/bptree/v0.BPTree, tierRatio struct{Tier1 uint64; Tier2 uint64; Tier3 uint64}, counts [4]uint64, lastRewardCacheTimestamp int64, currentEmission int64, getEmission func() (int64, .uverse.error), getHalvingBlocksInRange func(int64, int64) ([]int64, []int64, .uverse.error)) *gno.land/r/gnoswap/staker/v1.PoolTier

    • NewStakerV1(stakerStore interface {AddAllowedToken func(int, .uverse.realm, string) .uverse.error; AddDeniedRewardToken func(int, .uverse.realm, string) .uverse.error; GetAllowedTokens func() []string; GetCurrentSwapBatch func() *gno.land/r/gnoswap/staker.SwapBatchProcessor; GetDeniedRewardTokens func() []string; GetDepositGnsAmount func() int64; GetDeposits func() *gno.land/p/nt/bptree/v0.BPTree; GetExternalIncentives func() *gno.land/p/nt/bptree/v0.BPTree; GetIncentiveCounter func() *gno.land/r/gnoswap/staker.Counter; GetMinimumRewardAmount func() int64; GetPendingProtocolFee func(string) int64; GetPendingProtocolFees func() map[string]int64; GetPoolTierCounts func() [4]uint64; GetPoolTierCurrentEmission func() int64; GetPoolTierGetEmission func() func() (int64, .uverse.error); GetPoolTierGetHalvingBlocksInRange func() func(int64, int64) ([]int64, []int64, .uverse.error); GetPoolTierLastRewardCacheTimestamp func() int64; GetPoolTierMemberships func() *gno.land/p/nt/bptree/v0.BPTree; GetPoolTierRatio func() gno.land/r/gnoswap/staker.TierRatio; GetPools func() *gno.land/p/nt/bptree/v0.BPTree; GetTokenSpecificMinimumRewards func() map[string]int64; GetTotalEmissionSent func() int64; GetUncollectedIncentiveCounts func() *gno.land/p/nt/bptree/v0.BPTree; GetUnstakedPositions func() *gno.land/p/nt/bptree/v0.BPTree; GetUnstakingFee func() uint64; GetWarmupTemplate func() []gno.land/r/gnoswap/staker.Warmup; HasAllowedTokensStoreKey func() bool; HasCurrentSwapBatchStoreKey func() bool; HasDeniedRewardTokensStoreKey func() bool; HasDepositGnsAmountStoreKey func() bool; HasDepositsStoreKey func() bool; HasExternalIncentivesStoreKey func() bool; HasIncentiveCounterStoreKey func() bool; HasMinimumRewardAmountStoreKey func() bool; HasPendingProtocolFeesStoreKey func() bool; HasPoolTierCountsStoreKey func() bool; HasPoolTierCurrentEmissionStoreKey func() bool; HasPoolTierGetEmissionStoreKey func() bool; HasPoolTierGetHalvingBlocksInRangeStoreKey func() bool; HasPoolTierLastRewardCacheTimestampStoreKey func() bool; HasPoolTierMembershipsStoreKey func() bool; HasPoolTierRatioStoreKey func() bool; HasPoolsStoreKey func() bool; HasTokenSpecificMinimumRewardsStoreKey func() bool; HasTotalEmissionSentStoreKey func() bool; HasUncollectedIncentiveCountsStoreKey func() bool; HasUnstakedPositionsStoreKey func() bool; HasUnstakingFeeStoreKey func() bool; HasWarmupTemplateStoreKey func() bool; NextIncentiveID func(.uverse.address, int64) string; RemoveAllowedToken func(int, .uverse.realm, string) .uverse.error; RemoveDeniedRewardToken func(int, .uverse.realm, string) .uverse.error; RemovePendingProtocolFee func(int, .uverse.realm, string) .uverse.error; RemoveTokenSpecificMinimumRewardItem func(int, .uverse.realm, string) .uverse.error; SetAllowedTokens func(int, .uverse.realm, []string) .uverse.error; SetCurrentSwapBatch func(int, .uverse.realm, *gno.land/r/gnoswap/staker.SwapBatchProcessor) .uverse.error; SetDepositGnsAmount func(int, .uverse.realm, int64) .uverse.error; SetDeposits func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetExternalIncentives func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetIncentiveCounter func(int, .uverse.realm, *gno.land/r/gnoswap/staker.Counter) .uverse.error; SetMinimumRewardAmount func(int, .uverse.realm, int64) .uverse.error; SetPendingProtocolFee func(int, .uverse.realm, string, int64) .uverse.error; SetPendingProtocolFees func(int, .uverse.realm, map[string]int64) .uverse.error; SetPoolTierCounts func(int, .uverse.realm, [4]uint64) .uverse.error; SetPoolTierCurrentEmission func(int, .uverse.realm, int64) .uverse.error; SetPoolTierGetEmission func(int, .uverse.realm, func() (int64, .uverse.error)) .uverse.error; SetPoolTierGetHalvingBlocksInRange func(int, .uverse.realm, func(int64, int64) ([]int64, []int64, .uverse.error)) .uverse.error; SetPoolTierLastRewardCacheTimestamp func(int, .uverse.realm, int64) .uverse.error; SetPoolTierMemberships func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetPoolTierRatio func(int, .uverse.realm, gno.land/r/gnoswap/staker.TierRatio) .uverse.error; SetPools func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetTokenSpecificMinimumRewardItem func(int, .uverse.realm, string, int64) .uverse.error; SetTokenSpecificMinimumRewards func(int, .uverse.realm, map[string]int64) .uverse.error; SetTotalEmissionSent func(int, .uverse.realm, int64) .uverse.error; SetUncollectedIncentiveCounts func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetUnstakedPositions func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetUnstakingFee func(int, .uverse.realm, uint64) .uverse.error; SetWarmupTemplate func(int, .uverse.realm, []gno.land/r/gnoswap/staker.Warmup) .uverse.error}, poolAccessor interface {ExistsPoolPath func(string) bool; GetSlot0SqrtPriceX96 func(string) string; GetSlot0Tick func(string) int32; SetSwapEndHook func(int, .uverse.realm, func(int, .uverse.realm, string) .uverse.error); SetSwapStartHook func(int, .uverse.realm, func(int, .uverse.realm, string, int64)); SetTickCrossHook func(int, .uverse.realm, func(int, .uverse.realm, string, int32, bool, int64))}, emissionAccessor interface {GetStakerEmissionAmountPerSecond func() (int64, .uverse.error); GetStakerEmissionAmountPerSecondInRange func(int64, int64) ([]int64, []int64, .uverse.error); MintAndDistributeGns func(int, .uverse.realm) (int64, bool); SetOnDistributionPctChangeCallback func(int, .uverse.realm, func(int, .uverse.realm, int64))}, nftAccessor interface {Approve func(int, .uverse.realm, .uverse.address, gno.land/p/nt/grc721/v0.TokenID) .uverse.error; Burn func(int, .uverse.realm, gno.land/p/nt/grc721/v0.TokenID); Exists func(gno.land/p/nt/grc721/v0.TokenID) bool; Mint func(int, .uverse.realm, .uverse.address, gno.land/p/nt/grc721/v0.TokenID) gno.land/p/nt/grc721/v0.TokenID; MustOwnerOf func(gno.land/p/nt/grc721/v0.TokenID) .uverse.address; OwnerOf func(gno.land/p/nt/grc721/v0.TokenID) (.uverse.address, .uverse.error); TotalSupply func() int64; TransferFrom func(int, .uverse.realm, .uverse.address, .uverse.address, gno.land/p/nt/grc721/v0.TokenID) .uverse.error})

    • NewTickCrossEventInfo(tickID int32, stakedLiquidityGross *gno.land/p/gnoswap/uint256/v1.Uint, stakedLiquidityDelta *gno.land/p/gnoswap/int256/v1.Int, outsideAccumulation *gno.land/p/gnoswap/uint256/v1.Uint) *gno.land/r/gnoswap/staker/v1.tickCrossEventInfo

    • NewTickResolver(tick *gno.land/r/gnoswap/staker.Tick) *gno.land/r/gnoswap/staker/v1.TickResolver

    • NewUnstakedPositions() *gno.land/r/gnoswap/staker/v1.UnstakedPositions

    • TierRatioFromCounts(tier1Count uint64, tier2Count uint64, tier3Count uint64) struct{Tier1 uint64; Tier2 uint64; Tier3 uint64}

    Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

    Rendered

    RenderedRawgnoweb ↗
    This realm does not provide renderable output (vm/qrender → NoRenderDeclError). View on gnoweb ↗

    vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.

    9 sr "gno.land/r/gnoswap/staker"
    10)
    11
    12// Reward is a struct for storing reward for a position.
    13// Internal reward is the GNS reward, external reward is the reward for other incentives.
    14// Penalties are the amount that is deducted from the reward due to the position's warmup.
    15type Reward struct {
    16 Internal int64
    17 InternalPenalty int64
    18 External map[string]int64 // Incentive ID -> TokenAmount
    19 ExternalPenalty map[string]int64 // Incentive ID -> TokenAmount
    20}
    21
    22// aggregateRewards sums the per-warmup rewards/penalties into a single Reward.
    23func aggregateRewards(rewards []Reward) Reward {
    24 internal := int64(0)
    25 internalPenalty := int64(0)
    26
    27 rewardLen := len(rewards)
    28 externalReward := make(map[string]int64, rewardLen)
    29 externalPenalty := make(map[string]int64, rewardLen)
    30
    31 for _, reward := range rewards {
    32 internal = gnsmath.SafeAddInt64(internal, reward.Internal)
    33 internalPenalty = gnsmath.SafeAddInt64(internalPenalty, reward.InternalPenalty)
    34
    35 for incentive, amount := range reward.External {
    36 externalReward[incentive] = gnsmath.SafeAddInt64(externalReward[incentive], amount)
    37 }
    38
    39 for incentive, penalty := range reward.ExternalPenalty {
    40 externalPenalty[incentive] = gnsmath.SafeAddInt64(externalPenalty[incentive], penalty)
    41 }
    42 }
    43
    44 return Reward{
    45 Internal: internal,
    46 InternalPenalty: internalPenalty,
    47 External: externalReward,
    48 ExternalPenalty: externalPenalty,
    49 }
    50}
    51
    52// calculatePositionRewardParam is a struct for calculating position reward
    53type calculatePositionRewardParam struct {
    54 // Environmental variables
    55 CurrentHeight int64
    56 CurrentTime int64
    57 Deposits *Deposits
    58 Pools *Pools
    59 PoolTier *PoolTier
    60
    61 // Position variables
    62 PositionId uint64
    63
    64 // Deposit overrides the Deposits lookup for a deposit that is no longer in the tree.
    65 Deposit *sr.Deposit
    66 // Exit pins the pool state to the moment the position left the pool; nil while staked.
    67 Exit *sr.UnstakedPosition
    68}
    69
    70// deposit resolves the deposit the calculation runs against.
    71func (self *calculatePositionRewardParam) deposit() *sr.Deposit {
    72 if self.Deposit != nil {
    73 return self.Deposit
    74 }
    75
    76 return self.Deposits.get(self.PositionId)
    77}
    78
    79// positionRewardUpdate carries the persisted-state changes produced (but not applied) by a calculation.
    80// They are applied by the update* functions, so that only the collect path mutates state while the
    81// calculation stays pure.
    82type positionRewardUpdate struct {
    83 poolPath string
    84 pool *sr.Pool
    85 poolExisted bool
    86
    87 // External incentive ids discovered as newly created since the deposit's last update; they must be
    88 // added to the deposit's incentive index.
    89 newExternalIncentiveIds []string
    90 // Whether the deposit's LastExternalIncentiveUpdatedAt cursor should be advanced to CurrentTime.
    91 advanceExternalIncentiveCursor bool
    92
    93 // Incentive ids the calculation actually produced a reward for, in calculation order. The collect
    94 // path walks this instead of iterating the reward map, so delivery order is deterministic.
    95 externalIncentiveIds []string
    96}
    97
    98// positionRewardContext carries the per-position state that both the internal (GNS emission) and the
    99// external (incentive) reward calculations need. Resolving it once lets each path be calculated on its
    100// own without duplicating the deposit/pool/reward-state setup.
    101type positionRewardContext struct {
    102 deposit *sr.Deposit
    103 depositResolver *DepositResolver
    104 poolResolver *PoolResolver
    105 rewardState *RewardState
    106 warmupLen int
    107
    108 update positionRewardUpdate
    109}
    110
    111// newPositionRewardContext resolves the deposit, its pool and a fresh RewardState WITHOUT mutating
    112// persisted state. A missing pool is created ephemerally and its persistence is deferred to the
    113// update path.
    114func (s *stakerV1) newPositionRewardContext(param *calculatePositionRewardParam) *positionRewardContext {
    115 deposit := param.deposit()
    116 poolPath := deposit.TargetPoolPath()
    117
    118 pool, poolExisted := param.Pools.Get(poolPath)
    119 if !poolExisted {
    120 // Read-only: use an ephemeral pool; persistence is deferred to updatePositionReward.
    121 pool = sr.NewPool(poolPath, param.CurrentTime)
    122 }
    123 poolResolver := newPoolResolverWithExit(pool, param.Exit)
    124
    125 return &positionRewardContext{
    126 deposit: deposit,
    127 depositResolver: NewDepositResolver(deposit),
    128 poolResolver: poolResolver,
    129 rewardState: poolResolver.RewardStateOf(deposit),
    130 warmupLen: len(deposit.Warmups()),
    131 update: positionRewardUpdate{
    132 poolPath: poolPath,
    133 pool: pool,
    134 poolExisted: poolExisted,
    135 },
    136 }
    137}
    138
    139// newRewards allocates the empty per-warmup reward slice the compute* methods write into.
    140func (self *positionRewardContext) newRewards() []Reward {
    141 rewards := make([]Reward, self.warmupLen)
    142 for i := 0; i < self.warmupLen; i++ {
    143 rewards[i] = Reward{
    144 External: make(map[string]int64),
    145 ExternalPenalty: make(map[string]int64),
    146 }
    147 }
    148
    149 return rewards
    150}
    151
    152// computeInternalRewards writes the GNS emission reward and penalty of each warmup into rewards.
    153//
    154// The per-second emission-rate schedule is resolved up-front (resolveInternalRewardSegments) and fed to
    155// the single internal-reward calculator, so the calculation needs no reward-cache update.
    156func (self *positionRewardContext) computeInternalRewards(param *calculatePositionRewardParam, rewards []Reward) error {
    157 lastCollectTime := self.depositResolver.InternalRewardLastCollectTime()
    158
    159 // Resolve the per-second reward-rate schedule (pure) and calculate internal rewards from it.
    160 internalSegments, err := self.poolResolver.resolveInternalRewardSegments(param.PoolTier, self.update.poolPath, lastCollectTime, param.CurrentTime)
    161 if err != nil {
    162 return err
    163 }
    164 calculatedInternalRewards, calculatedInternalPenalties := self.rewardState.calculateInternalReward(internalSegments)
    165
    166 for i := 0; i < self.warmupLen; i++ {
    167 rewards[i].Internal = calculatedInternalRewards[i]
    168 rewards[i].InternalPenalty = calculatedInternalPenalties[i]
    169 }
    170
    171 self.rewardState.reset()
    172
    173 return nil
    174}
    175
    176// discoverExternalIncentives records the incentives created since the deposit's last update on the
    177// update, WITHOUT mutating the deposit. The collect path adds them to the deposit's incentive index.
    178//
    179// ExternalRewardLastCollectTime falls back to StakeTime for ids not yet persisted, so a newly-discovered
    180// incentive yields the same calculation whether or not it is written to the deposit.
    181func (self *positionRewardContext) discoverExternalIncentives(param *calculatePositionRewardParam) {
    182 lastExternalIncentiveUpdatedAt := self.depositResolver.LastExternalIncentiveUpdatedAt()
    183 if lastExternalIncentiveUpdatedAt >= param.CurrentTime {
    184 return
    185 }
    186
    187 // Discover incentives from this pool's own start-time index. Using the
    188 // local resolver keeps calculation read-only even for an ephemeral pool
    189 // that has not yet been persisted in param.Pools.
    190 newIds := make([]string, 0)
    191 self.poolResolver.IncentivesResolver().IterateIncentiveIdsByTime(lastExternalIncentiveUpdatedAt, param.CurrentTime, func(incentiveId string) bool {
    192 newIds = append(newIds, incentiveId)
    193 return false
    194 })
    195
    196 self.update.newExternalIncentiveIds = newIds
    197 self.update.advanceExternalIncentiveCursor = true
    198}
    199
    200// externalIncentiveIds returns the deposit's effective incentive-id set: the ids stored on the deposit
    201// plus the ones discoverExternalIncentives found. Call it after discoverExternalIncentives.
    202func (self *positionRewardContext) externalIncentiveIds() []string {
    203 seen := make(map[string]bool)
    204 incentiveIds := make([]string, 0)
    205
    206 self.deposit.IterateExternalIncentiveIds(func(incentiveId string) bool {
    207 if !seen[incentiveId] {
    208 seen[incentiveId] = true
    209 incentiveIds = append(incentiveIds, incentiveId)
    210 }
    211 return false
    212 })
    213
    214 for _, incentiveId := range self.update.newExternalIncentiveIds {
    215 if !seen[incentiveId] {
    216 seen[incentiveId] = true
    217 incentiveIds = append(incentiveIds, incentiveId)
    218 }
    219 }
    220
    221 return incentiveIds
    222}
    223
    224// computeExternalReward writes one incentive's external reward and penalty of each warmup into rewards.
    225// An incentive that does not exist on the pool or has not started yet contributes nothing.
    226func (self *positionRewardContext) computeExternalReward(param *calculatePositionRewardParam, rewards []Reward, incentiveId string) {
    227 incentive, ok := self.poolResolver.IncentivesResolver().Get(incentiveId)
    228 if !ok {
    229 return
    230 }
    231
    232 incentiveResolver := NewExternalIncentiveResolver(incentive)
    233
    234 // Check if incentive is active during this specific collection period
    235 if !incentiveResolver.IsStarted(param.CurrentTime) {
    236 return
    237 }
    238
    239 // External incentivized pool.
    240 // Calculate reward for each warmup using per-incentive lastCollectTime
    241 externalLastCollectTime := self.depositResolver.ExternalRewardLastCollectTime(incentiveId)
    242 externalReward, externalPenalty := self.rewardState.calculateExternalReward(externalLastCollectTime, param.CurrentTime, incentive)
    243
    244 for i := range externalReward {
    245 if externalReward[i] > 0 || externalPenalty[i] > 0 {
    246 rewards[i].External[incentiveId] = externalReward[i]
    247 rewards[i].ExternalPenalty[incentiveId] = externalPenalty[i]
    248 }
    249 }
    250
    251 self.update.externalIncentiveIds = append(self.update.externalIncentiveIds, incentiveId)
    252 self.rewardState.reset()
    253}
    254
    255// computeExternalRewards writes every incentive of the deposit into rewards.
    256func (self *positionRewardContext) computeExternalRewards(param *calculatePositionRewardParam, rewards []Reward) {
    257 self.discoverExternalIncentives(param)
    258
    259 for _, incentiveId := range self.externalIncentiveIds() {
    260 self.computeExternalReward(param, rewards, incentiveId)
    261 }
    262}
    263
    264// calculateCollectablePositionReward calculates the aggregated position reward WITHOUT mutating any persisted state.
    265//
    266// It is the shared, read-only entry point used by the Collectable* view getters.
    267// This keeps the calculation identical for every caller and guarantees views never write.
    268func (s *stakerV1) calculateCollectablePositionReward(currentHeight, currentTimestamp int64, positionId uint64) (Reward, error) {
    269 param := &calculatePositionRewardParam{
    270 CurrentHeight: currentHeight,
    271 CurrentTime: currentTimestamp,
    272 Deposits: s.getDeposits(),
    273 Pools: s.getPools(),
    274 PoolTier: s.getPoolTier(),
    275 PositionId: positionId,
    276 }
    277
    278 // An unstaked position quotes against its exit checkpoint, at its exit time.
    279 if checkpoint := s.getUnstakedPositions().get(positionId); checkpoint != nil {
    280 param.CurrentTime = checkpoint.ExitTime()
    281 param.Deposit = checkpoint.Deposit()
    282 param.Exit = checkpoint
    283 }
    284
    285 rewards, _, err := s.calculatePositionReward(param)
    286 if err != nil {
    287 return Reward{}, err
    288 }
    289
    290 return aggregateRewards(rewards), nil
    291}
    292
    293// calculatePositionReward computes a position's per-warmup internal AND external rewards WITHOUT
    294// mutating persisted state. All would-be state changes are returned as a positionRewardUpdate for the
    295// caller to apply (collect only).
    296func (s *stakerV1) calculatePositionReward(param *calculatePositionRewardParam) ([]Reward, positionRewardUpdate, error) {
    297 ctx := s.newPositionRewardContext(param)
    298 rewards := ctx.newRewards()
    299
    300 if err := ctx.computeInternalRewards(param, rewards); err != nil {
    301 return nil, positionRewardUpdate{}, err
    302 }
    303 ctx.computeExternalRewards(param, rewards)
    304
    305 return rewards, ctx.update, nil
    306}
    307
    308// calculateExternalPositionRewards computes all of a position's external incentive rewards WITHOUT
    309// mutating persisted state. Used by the combined collect path while GNS emission is halted.
    310func (s *stakerV1) calculateExternalPositionRewards(param *calculatePositionRewardParam) ([]Reward, positionRewardUpdate) {
    311 ctx := s.newPositionRewardContext(param)
    312 rewards := ctx.newRewards()
    313
    314 ctx.computeExternalRewards(param, rewards)
    315
    316 return rewards, ctx.update
    317}
    318
    319// calculateInternalPositionReward computes only a position's per-warmup GNS emission rewards, WITHOUT
    320// mutating persisted state. Used by the emission collect path.
    321func (s *stakerV1) calculateInternalPositionReward(param *calculatePositionRewardParam) ([]Reward, positionRewardUpdate, error) {
    322 ctx := s.newPositionRewardContext(param)
    323 rewards := ctx.newRewards()
    324
    325 if err := ctx.computeInternalRewards(param, rewards); err != nil {
    326 return nil, positionRewardUpdate{}, err
    327 }
    328
    329 return rewards, ctx.update, nil
    330}
    331
    332// calculateExternalPositionReward computes a position's per-warmup reward for one external incentive,
    333// WITHOUT mutating persisted state. Used by the external incentive collect path.
    334//
    335// Incentive discovery still runs over every incentive, so the deposit's incentive index stays complete
    336// no matter which incentive the caller asked for.
    337func (s *stakerV1) calculateExternalPositionReward(param *calculatePositionRewardParam, incentiveId string) ([]Reward, positionRewardUpdate) {
    338 ctx := s.newPositionRewardContext(param)
    339 rewards := ctx.newRewards()
    340
    341 ctx.discoverExternalIncentives(param)
    342 ctx.computeExternalReward(param, rewards, incentiveId)
    343
    344 return rewards, ctx.update
    345}
    346
    347// persistLazyPool persists a pool that calculation created ephemerally because it did not exist yet.
    348func (s *stakerV1) persistLazyPool(param *calculatePositionRewardParam, updateParams positionRewardUpdate) {
    349 if !updateParams.poolExisted {
    350 param.Pools.set(updateParams.poolPath, updateParams.pool)
    351 }
    352}
    353
    354// updateInternalRewardCache advances the reward cache up to CurrentTime (halving boundaries). The
    355// calculation itself no longer needs this, but downstream unclaimable processing (which reads
    356// CurrentReward) and off-chain history rely on the cache, so it is advanced on the emission collect path.
    357func (s *stakerV1) updateInternalRewardCache(param *calculatePositionRewardParam, updateParams positionRewardUpdate) {
    358 // Always at the current time, never at param.CurrentTime: the cache and the unclaimable
    359 // tracking it drives are pool-global, and a checkpoint collect would rewind them to its exit
    360 // timestamp, where the pool may have had no staked liquidity.
    361 param.PoolTier.cacheRewardForPool(time.Now().Unix(), param.Pools, updateParams.poolPath)
    362}
    363
    364// applyExternalIncentiveIndex persists the deposit incentive-index updates discovered during calculation.
    365func (s *stakerV1) applyExternalIncentiveIndex(param *calculatePositionRewardParam, updateParams positionRewardUpdate) {
    366 if len(updateParams.newExternalIncentiveIds) == 0 && !updateParams.advanceExternalIncentiveCursor {
    367 return
    368 }
    369
    370 deposit := param.deposit()
    371 for _, incentiveId := range updateParams.newExternalIncentiveIds {
    372 deposit.AddExternalIncentiveId(incentiveId)
    373 }
    374 if updateParams.advanceExternalIncentiveCursor {
    375 deposit.SetLastExternalIncentiveUpdatedAt(param.CurrentTime)
    376 }
    377}
    378
    379// updatePositionReward applies the persisted-state changes produced by calculatePositionReward.
    380// It is called ONLY by the collect path; the Collectable* view getters discard the update.
    381func (s *stakerV1) updatePositionReward(param *calculatePositionRewardParam, updateParams positionRewardUpdate) {
    382 s.persistLazyPool(param, updateParams)
    383 s.updateInternalRewardCache(param, updateParams)
    384 s.applyExternalIncentiveIndex(param, updateParams)
    385}
    386
    387// updateInternalPositionReward applies the state changes belonging to the emission collect path only.
    388func (s *stakerV1) updateInternalPositionReward(param *calculatePositionRewardParam, updateParams positionRewardUpdate) {
    389 s.persistLazyPool(param, updateParams)
    390 s.updateInternalRewardCache(param, updateParams)
    391}
    392
    393// updateExternalPositionReward applies the state changes belonging to the external collect path only.
    394func (s *stakerV1) updateExternalPositionReward(param *calculatePositionRewardParam, updateParams positionRewardUpdate) {
    395 s.persistLazyPool(param, updateParams)
    396 s.applyExternalIncentiveIndex(param, updateParams)
    397}
    398
    399// internalRewardSegment is a [start, end) span over which the per-second emission reward rate is constant.
    400type internalRewardSegment struct {
    401 start int64
    402 end int64
    403 rewardPerSecond int64
    404}
    405
    406// resolveInternalRewardSegments builds the per-second reward-rate schedule over [startTime, endTime]
    407// WITHOUT mutating state.
    408//
    409// Persisted reward-cache entries cover the historical portion: they record tier/count changes and any
    410// halvings already materialized by past collects, so no un-materialized halving exists strictly between
    411// two persisted entries. The tail beyond the last persisted entry is split by halvings using the current
    412// tier ratio/count, which are necessarily constant there (a change would have written a cache entry). The
    413// tail rate is recomputed with the same calculatePoolReward arithmetic the cache writer uses, so a
    414// schedule resolved from a fully materialized cache and one resolved from the emission halvings are
    415// identical.
    416func (self *PoolResolver) resolveInternalRewardSegments(poolTier *PoolTier, poolPath string, startTime, endTime int64) ([]internalRewardSegment, error) {
    417 segments := make([]internalRewardSegment, 0)
    418 if startTime >= endTime {
    419 return segments, nil
    420 }
    421
    422 currentReward := self.CurrentReward(startTime)
    423 cursor := startTime
    424
    425 self.RewardCache().Iterate(startTime, endTime, func(key int64, value any) bool {
    426 reward, ok := value.(int64)
    427 if !ok {
    428 panic(ufmt.Sprintf("failed to cast value to int64: %T", value))
    429 }
    430
    431 segments = append(segments, internalRewardSegment{start: cursor, end: key, rewardPerSecond: currentReward})
    432 cursor = key
    433 currentReward = reward
    434 return false
    435 })
    436
    437 if cursor < endTime {
    438 var err error
    439 segments, err = appendInternalRewardTailSegments(segments, poolTier, poolPath, cursor, endTime, currentReward, self.exit)
    440 if err != nil {
    441 return nil, err
    442 }
    443 }
    444
    445 return segments, nil
    446}
    447
    448// appendInternalRewardTailSegments appends the schedule for the tail [startTime, endTime], where no
    449// persisted cache entry exists beyond startTime. Over this span tier/count are constant, so the rate
    450// changes only at halving boundaries.
    451func appendInternalRewardTailSegments(segments []internalRewardSegment, poolTier *PoolTier, poolPath string, startTime, endTime, baseReward int64, exit *sr.UnstakedPosition) ([]internalRewardSegment, error) {
    452 // A checkpoint reads the tier context it exited under: this window closed at the exit, and
    453 // the live lookup is skipped so a checkpoint collect never depends on live tier state.
    454 var tier, ratio, count uint64
    455 if exit != nil {
    456 tier, ratio, count = exit.Tier(), exit.TierRatio(), exit.TierCount()
    457 } else {
    458 var err error
    459 tier, ratio, count, err = poolTier.tierContextOf(poolPath)
    460 if err != nil {
    461 return nil, err
    462 }
    463 }
    464
    465 if tier == 0 || tier >= AllTierCount {
    466 // Not currently tiered: the rate cannot increase; the base is 0 after de-tier.
    467 return append(segments, internalRewardSegment{start: startTime, end: endTime, rewardPerSecond: baseReward}), nil
    468 }
    469
    470 tierRatioInt64 := int64(ratio)
    471 tierCount := int64(count)
    472
    473 halvingTimestamps, halvingEmissions, err := poolTier.getHalvingBlocksInRange(startTime, endTime)
    474 if err != nil {
    475 return nil, err
    476 }
    477
    478 segStart := startTime
    479 rate := baseReward
    480 for i, hv := range halvingTimestamps {
    481 if hv <= segStart {
    482 // Halving effective at/before the segment start: only switch the rate.
    483 rate, err = calculatePoolReward(halvingEmissions[i], tierRatioInt64, tierCount)
    484 if err != nil {
    485 return nil, err
    486 }
    487 continue
    488 }
    489 if hv >= endTime {
    490 break
    491 }
    492
    493 segments = append(segments, internalRewardSegment{start: segStart, end: hv, rewardPerSecond: rate})
    494 rate, err = calculatePoolReward(halvingEmissions[i], tierRatioInt64, tierCount)
    495 if err != nil {
    496 return nil, err
    497 }
    498 segStart = hv
    499 }
    500
    501 return append(segments, internalRewardSegment{start: segStart, end: endTime, rewardPerSecond: rate}), nil
    502}
    503
    504// calculates internal unclaimable reward for the pool
    505func (s *stakerV1) processUnClaimableReward(poolPath string, endTimestamp int64) int64 {
    506 pool, ok := s.getPools().Get(poolPath)
    507 if !ok {
    508 return 0
    509 }
    510 poolResolver := NewPoolResolver(pool)
    511
    512 return poolResolver.processUnclaimableReward(endTimestamp)
    513}
    514
    515// update deposit's incentive list with new incentives created since last update
    516func (s *stakerV1) getExternalIncentiveIdsBy(poolPath string, startTime, endTime int64) []string {
    517 currentIncentiveIds := make([]string, 0)
    518
    519 pool, ok := s.getPools().Get(poolPath)
    520 if !ok {
    521 return currentIncentiveIds
    522 }
    523 poolResolver := NewPoolResolver(pool)
    524
    525 // Look up the pool's own start-time index instead of a global
    526 // creation-time index. The index is scoped to this pool's incentives, so
    527 // discovery cost is bounded by the number of incentives for this pool
    528 // within the queried range, and no longer grows with the total number of
    529 // incentives system-wide.
    530 poolResolver.IncentivesResolver().IterateIncentiveIdsByTime(startTime, endTime, func(incentiveId string) bool {
    531 currentIncentiveIds = append(currentIncentiveIds, incentiveId)
    532 return false
    533 })
    534
    535 return currentIncentiveIds
    536}
    537
    538// getInitialCollectTime determines the initial collection time for an incentive
    539// by taking the maximum of the deposit's stake time and the incentive's start time.
    540// This ensures rewards are only calculated from when both conditions are met:
    541// - The position must be staked (deposit.stakeTime)
    542// - The incentive must be active (incentive.startTimestamp)
    543//
    544// This function is used for lazy initialization when a position collects
    545// from an incentive for the first time, avoiding the need to iterate through
    546// all deposits when a new incentive is created.
    547func getInitialCollectTime(deposit *sr.Deposit, incentive *sr.ExternalIncentive) int64 {
    548 if deposit.StakeTime() > incentive.StartTimestamp() {
    549 return deposit.StakeTime()
    550 }
    551 return incentive.StartTimestamp()
    552}
    553
    *gno.land/r/gnoswap/staker/v1.stakerV1