Skip to content

Commit 7f2a846

Browse files
authored
feat(app): tax and commision (#5208)
- **feat(app): add tax and commisions** - **chore(app): fmt** - **fix(app): use constant and get address dynamically from config**
2 parents aa7e964 + 7d23c21 commit 7f2a846

File tree

2 files changed

+178
-18
lines changed

2 files changed

+178
-18
lines changed

app2/src/lib/services/incentive.ts

Lines changed: 178 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
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

1310
import { 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

1614
const 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+
5193
export 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+
134235
export 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,

app2/src/routes/stake/+page.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@ AppRuntime.runPromiseExit$(() => {
129129
const incentives = AppRuntime.runPromiseExit$(() => {
130130
return Effect.gen(function*() {
131131
const incentive = yield* calculateIncentive
132-
console.log("Incentive data loaded:", incentive)
133132
return incentive
134133
}).pipe(
135134
Effect.provide(FetchHttpClient.layer),

0 commit comments

Comments
 (0)