11/**
2- * Incentive Service (WORK IN PROGRESS)
2+ * Incentive Service
33 *
4- * Calculates staking incentives using:
5- * - Total supply from cosmos bank module
6- * - Inflation rate from cosmos mint module
7- * - Bonded token supply from staking pool
8- * - Community tax from distribution params
9- *
10- * Formula: Incentive = ((1 + [(inflation × total_supply ÷ bonded) × (1 − tax)] ÷ 365) ^ 365) − 1
4+ * Calculates liquid staking incentives including:
5+ * - Base staking rewards (inflation × total_supply ÷ bonded_tokens)
6+ * - Community tax deduction
7+ * - Validator commission deduction (weighted average from delegated validators)
118 */
129
1310import { HttpClient , HttpClientResponse } from "@effect/platform"
14- import { BigDecimal , Data , Effect , pipe , Schema } from "effect"
11+ import { EU_STAKING_HUB } from "@unionlabs/sdk/Constants"
12+ import { Array , BigDecimal , Data , Effect , Option as O , pipe , Schema } from "effect"
1513
1614const REST_BASE_URL = "https://rest.union.build"
1715
@@ -47,13 +45,61 @@ const CirculatingSupplyResponse = Schema.Struct({
4745 } ) ,
4846} )
4947
50- // Schema for the incentive calculation result
48+ const ValidatorsResponse = Schema . Struct ( {
49+ validators : Schema . Array ( Schema . Struct ( {
50+ operator_address : Schema . String ,
51+ tokens : Schema . BigDecimal ,
52+ commission : Schema . Struct ( {
53+ commission_rates : Schema . Struct ( {
54+ rate : Schema . BigDecimal ,
55+ } ) ,
56+ } ) ,
57+ status : Schema . String ,
58+ jailed : Schema . Boolean ,
59+ } ) ) ,
60+ } )
61+
62+ const DelegatorDelegationsResponse = Schema . Struct ( {
63+ delegation_responses : Schema . Array ( Schema . Struct ( {
64+ delegation : Schema . Struct ( {
65+ delegator_address : Schema . String ,
66+ validator_address : Schema . String ,
67+ shares : Schema . BigDecimal ,
68+ } ) ,
69+ balance : Schema . Struct ( {
70+ denom : Schema . String ,
71+ amount : Schema . BigDecimal ,
72+ } ) ,
73+ } ) ) ,
74+ } )
75+
76+ const LstConfigResponse = Schema . Struct ( {
77+ data : Schema . Struct ( {
78+ staker_address : Schema . String ,
79+ native_token_denom : Schema . String ,
80+ minimum_liquid_stake_amount : Schema . String ,
81+ protocol_fee_config : Schema . Struct ( {
82+ fee_rate : Schema . String ,
83+ fee_recipient : Schema . String ,
84+ } ) ,
85+ monitors : Schema . Array ( Schema . String ) ,
86+ lst_address : Schema . String ,
87+ batch_period_seconds : Schema . Number ,
88+ unbonding_period_seconds : Schema . Number ,
89+ stopped : Schema . Boolean ,
90+ } ) ,
91+ } )
92+
5193export const IncentiveResult = Schema . Struct ( {
5294 rates : Schema . Struct ( {
5395 yearly : Schema . BigDecimalFromSelf ,
5496 } ) ,
5597 incentiveNominal : Schema . BigDecimalFromSelf ,
5698 incentiveAfterTax : Schema . BigDecimalFromSelf ,
99+ incentiveAfterCommission : Schema . BigDecimalFromSelf ,
100+ communityTaxAmount : Schema . BigDecimalFromSelf ,
101+ validatorCommissionAmount : Schema . BigDecimalFromSelf ,
102+ weightedAverageCommission : Schema . BigDecimalFromSelf ,
57103 inflation : Schema . BigDecimalFromSelf ,
58104 totalSupply : Schema . BigDecimalFromSelf ,
59105 bondedTokens : Schema . BigDecimalFromSelf ,
@@ -131,17 +177,85 @@ const getCirculatingSupply = pipe(
131177 ) ,
132178)
133179
180+ const getValidators = pipe (
181+ HttpClient . HttpClient ,
182+ Effect . map ( HttpClient . withTracerDisabledWhen ( ( ) => true ) ) ,
183+ Effect . andThen ( ( client ) =>
184+ pipe (
185+ client . get ( `${ REST_BASE_URL } /cosmos/staking/v1beta1/validators?status=BOND_STATUS_BONDED` ) ,
186+ Effect . flatMap ( HttpClientResponse . schemaBodyJson ( ValidatorsResponse ) ) ,
187+ Effect . mapError ( ( cause ) =>
188+ new IncentiveError ( {
189+ message : "Failed to fetch validators" ,
190+ cause,
191+ } )
192+ ) ,
193+ )
194+ ) ,
195+ )
196+
197+ const getLstConfig = pipe (
198+ HttpClient . HttpClient ,
199+ Effect . map ( HttpClient . withTracerDisabledWhen ( ( ) => true ) ) ,
200+ Effect . andThen ( ( client ) => {
201+ const queryMsg = btoa ( JSON . stringify ( { config : { } } ) )
202+ return pipe (
203+ client . get (
204+ `${ REST_BASE_URL } /cosmwasm/wasm/v1/contract/${ EU_STAKING_HUB . address } /smart/${ queryMsg } ` ,
205+ ) ,
206+ Effect . flatMap ( HttpClientResponse . schemaBodyJson ( LstConfigResponse ) ) ,
207+ Effect . mapError ( ( cause ) =>
208+ new IncentiveError ( {
209+ message : "Failed to fetch LST contract config" ,
210+ cause,
211+ } )
212+ ) ,
213+ )
214+ } ) ,
215+ )
216+
217+ const getDelegatorDelegations = ( delegatorAddress : string ) =>
218+ pipe (
219+ HttpClient . HttpClient ,
220+ Effect . map ( HttpClient . withTracerDisabledWhen ( ( ) => true ) ) ,
221+ Effect . andThen ( ( client ) =>
222+ pipe (
223+ client . get ( `${ REST_BASE_URL } /cosmos/staking/v1beta1/delegations/${ delegatorAddress } ` ) ,
224+ Effect . flatMap ( HttpClientResponse . schemaBodyJson ( DelegatorDelegationsResponse ) ) ,
225+ Effect . mapError ( ( cause ) =>
226+ new IncentiveError ( {
227+ message : "Failed to fetch delegator delegations" ,
228+ cause,
229+ } )
230+ ) ,
231+ )
232+ ) ,
233+ )
234+
134235export const calculateIncentive : Effect . Effect <
135236 IncentiveResult ,
136237 IncentiveError ,
137238 HttpClient . HttpClient
138239> = Effect . gen ( function * ( ) {
139- const [ inflationData , stakingPoolData , distributionData , circulatingSupplyData ] = yield * Effect
240+ // First get the LST config to find the staker address
241+ const lstConfig = yield * getLstConfig
242+ const stakerAddress = lstConfig . data . staker_address
243+
244+ const [
245+ inflationData ,
246+ stakingPoolData ,
247+ distributionData ,
248+ circulatingSupplyData ,
249+ validatorsData ,
250+ delegationsData ,
251+ ] = yield * Effect
140252 . all ( [
141253 getInflation ,
142254 getStakingPool ,
143255 getDistributionParams ,
144256 getCirculatingSupply ,
257+ getValidators ,
258+ getDelegatorDelegations ( stakerAddress ) ,
145259 ] , { concurrency : "unbounded" } )
146260
147261 const inflation = inflationData . inflation
@@ -181,7 +295,6 @@ export const calculateIncentive: Effect.Effect<
181295 )
182296 }
183297
184- // Step 1: Calculate nominal incentive rate
185298 const incentiveNominal = yield * pipe (
186299 BigDecimal . multiply ( inflation , totalSupply ) ,
187300 BigDecimal . divide ( bondedTokens ) ,
@@ -192,11 +305,55 @@ export const calculateIncentive: Effect.Effect<
192305 ) ,
193306 )
194307
195- // Step 2: Apply community tax
196- const incentiveAfterTax = BigDecimal . multiply (
197- incentiveNominal ,
198- BigDecimal . subtract ( BigDecimal . fromBigInt ( 1n ) , communityTax ) ,
308+ const communityTaxAmount = BigDecimal . multiply ( incentiveNominal , communityTax )
309+ const incentiveAfterTax = BigDecimal . subtract ( incentiveNominal , communityTaxAmount )
310+
311+ // Calculate weighted average validator commission
312+ const validDelegations = pipe (
313+ delegationsData . delegation_responses ,
314+ Array . filterMap ( delegation => {
315+ const validator = pipe (
316+ validatorsData . validators ,
317+ Array . findFirst ( v => v . operator_address === delegation . delegation . validator_address ) ,
318+ )
319+
320+ return pipe (
321+ validator ,
322+ O . filter ( v => ! v . jailed && v . status === "BOND_STATUS_BONDED" ) ,
323+ O . map ( v => ( {
324+ amount : delegation . balance . amount ,
325+ commission : v . commission . commission_rates . rate ,
326+ } ) ) ,
327+ )
328+ } ) ,
329+ )
330+
331+ const { totalAmount, weightedSum } = pipe (
332+ validDelegations ,
333+ Array . reduce (
334+ { totalAmount : BigDecimal . fromBigInt ( 0n ) , weightedSum : BigDecimal . fromBigInt ( 0n ) } ,
335+ ( acc , { amount, commission } ) => ( {
336+ totalAmount : BigDecimal . sum ( acc . totalAmount , amount ) ,
337+ weightedSum : BigDecimal . sum ( acc . weightedSum , BigDecimal . multiply ( amount , commission ) ) ,
338+ } ) ,
339+ ) ,
340+ )
341+
342+ const weightedAverageCommission = BigDecimal . isZero ( totalAmount )
343+ ? BigDecimal . fromBigInt ( 0n )
344+ : yield * BigDecimal . divide ( weightedSum , totalAmount ) . pipe (
345+ Effect . mapError ( ( ) =>
346+ new IncentiveError ( {
347+ message : "Could not calculate weighted average commission" ,
348+ } )
349+ ) ,
350+ )
351+
352+ const validatorCommissionAmount = BigDecimal . multiply (
353+ incentiveAfterTax ,
354+ weightedAverageCommission ,
199355 )
356+ const incentiveAfterCommission = BigDecimal . subtract ( incentiveAfterTax , validatorCommissionAmount )
200357
201358 const bondedRatio = yield * BigDecimal . divide ( bondedTokens , totalSupply ) . pipe (
202359 Effect . mapError ( ( ) =>
@@ -208,10 +365,14 @@ export const calculateIncentive: Effect.Effect<
208365
209366 return {
210367 rates : {
211- yearly : incentiveAfterTax ,
368+ yearly : incentiveAfterCommission ,
212369 } ,
213370 incentiveNominal,
214371 incentiveAfterTax,
372+ incentiveAfterCommission,
373+ communityTaxAmount,
374+ validatorCommissionAmount,
375+ weightedAverageCommission,
215376 inflation,
216377 totalSupply,
217378 bondedTokens,
0 commit comments