Skip to content

Commit 9d14a08

Browse files
committed
feat(app): add tax and commisions
1 parent 32e56a6 commit 9d14a08

File tree

2 files changed

+123
-18
lines changed

2 files changed

+123
-18
lines changed

app2/src/lib/services/incentive.ts

Lines changed: 123 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
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 { Array, BigDecimal, Data, Effect, pipe, Schema } from "effect"
1512

1613
const REST_BASE_URL = "https://rest.union.build"
1714

15+
// Staker address that delegates to validators on behalf of liquid stakers
16+
const LIQUID_STAKING_STAKER_ADDRESS = "union19ydrfy0d80vgpvs6p0cljlahgxwrkz54ps8455q7jfdfape7ld7quaq69v"
17+
1818
export class IncentiveError extends Data.TaggedError("IncentiveError")<{
1919
message: string
2020
cause?: unknown
@@ -47,13 +47,44 @@ const CirculatingSupplyResponse = Schema.Struct({
4747
}),
4848
})
4949

50-
// Schema for the incentive calculation result
50+
const ValidatorsResponse = Schema.Struct({
51+
validators: Schema.Array(Schema.Struct({
52+
operator_address: Schema.String,
53+
tokens: Schema.BigDecimal,
54+
commission: Schema.Struct({
55+
commission_rates: Schema.Struct({
56+
rate: Schema.BigDecimal,
57+
}),
58+
}),
59+
status: Schema.String,
60+
jailed: Schema.Boolean,
61+
})),
62+
})
63+
64+
const DelegatorDelegationsResponse = Schema.Struct({
65+
delegation_responses: Schema.Array(Schema.Struct({
66+
delegation: Schema.Struct({
67+
delegator_address: Schema.String,
68+
validator_address: Schema.String,
69+
shares: Schema.BigDecimal,
70+
}),
71+
balance: Schema.Struct({
72+
denom: Schema.String,
73+
amount: Schema.BigDecimal,
74+
}),
75+
})),
76+
})
77+
5178
export const IncentiveResult = Schema.Struct({
5279
rates: Schema.Struct({
5380
yearly: Schema.BigDecimalFromSelf,
5481
}),
5582
incentiveNominal: Schema.BigDecimalFromSelf,
5683
incentiveAfterTax: Schema.BigDecimalFromSelf,
84+
incentiveAfterCommission: Schema.BigDecimalFromSelf,
85+
communityTaxAmount: Schema.BigDecimalFromSelf,
86+
validatorCommissionAmount: Schema.BigDecimalFromSelf,
87+
weightedAverageCommission: Schema.BigDecimalFromSelf,
5788
inflation: Schema.BigDecimalFromSelf,
5889
totalSupply: Schema.BigDecimalFromSelf,
5990
bondedTokens: Schema.BigDecimalFromSelf,
@@ -131,17 +162,53 @@ const getCirculatingSupply = pipe(
131162
),
132163
)
133164

165+
const getValidators = pipe(
166+
HttpClient.HttpClient,
167+
Effect.map(HttpClient.withTracerDisabledWhen(() => true)),
168+
Effect.andThen((client) =>
169+
pipe(
170+
client.get(`${REST_BASE_URL}/cosmos/staking/v1beta1/validators?status=BOND_STATUS_BONDED`),
171+
Effect.flatMap(HttpClientResponse.schemaBodyJson(ValidatorsResponse)),
172+
Effect.mapError((cause) =>
173+
new IncentiveError({
174+
message: "Failed to fetch validators",
175+
cause,
176+
})
177+
),
178+
)
179+
),
180+
)
181+
182+
const getDelegatorDelegations = (delegatorAddress: string) => pipe(
183+
HttpClient.HttpClient,
184+
Effect.map(HttpClient.withTracerDisabledWhen(() => true)),
185+
Effect.andThen((client) =>
186+
pipe(
187+
client.get(`${REST_BASE_URL}/cosmos/staking/v1beta1/delegations/${delegatorAddress}`),
188+
Effect.flatMap(HttpClientResponse.schemaBodyJson(DelegatorDelegationsResponse)),
189+
Effect.mapError((cause) =>
190+
new IncentiveError({
191+
message: "Failed to fetch delegator delegations",
192+
cause,
193+
})
194+
),
195+
)
196+
),
197+
)
198+
134199
export const calculateIncentive: Effect.Effect<
135200
IncentiveResult,
136201
IncentiveError,
137202
HttpClient.HttpClient
138203
> = Effect.gen(function*() {
139-
const [inflationData, stakingPoolData, distributionData, circulatingSupplyData] = yield* Effect
204+
const [inflationData, stakingPoolData, distributionData, circulatingSupplyData, validatorsData, delegationsData] = yield* Effect
140205
.all([
141206
getInflation,
142207
getStakingPool,
143208
getDistributionParams,
144209
getCirculatingSupply,
210+
getValidators,
211+
getDelegatorDelegations(LIQUID_STAKING_STAKER_ADDRESS),
145212
], { concurrency: "unbounded" })
146213

147214
const inflation = inflationData.inflation
@@ -181,7 +248,6 @@ export const calculateIncentive: Effect.Effect<
181248
)
182249
}
183250

184-
// Step 1: Calculate nominal incentive rate
185251
const incentiveNominal = yield* pipe(
186252
BigDecimal.multiply(inflation, totalSupply),
187253
BigDecimal.divide(bondedTokens),
@@ -192,12 +258,48 @@ export const calculateIncentive: Effect.Effect<
192258
),
193259
)
194260

195-
// Step 2: Apply community tax
196-
const incentiveAfterTax = BigDecimal.multiply(
197-
incentiveNominal,
198-
BigDecimal.subtract(BigDecimal.fromBigInt(1n), communityTax),
261+
const communityTaxAmount = BigDecimal.multiply(incentiveNominal, communityTax)
262+
const incentiveAfterTax = BigDecimal.subtract(incentiveNominal, communityTaxAmount)
263+
264+
// Calculate weighted average validator commission
265+
const validatorMap = new Map(validatorsData.validators.map(v => [v.operator_address, v]))
266+
267+
const validDelegations = pipe(
268+
delegationsData.delegation_responses,
269+
Array.filter(delegation => {
270+
const validator = validatorMap.get(delegation.delegation.validator_address)
271+
return Boolean(validator && !validator.jailed && validator.status === "BOND_STATUS_BONDED")
272+
}),
273+
Array.map(delegation => ({
274+
amount: delegation.balance.amount,
275+
commission: validatorMap.get(delegation.delegation.validator_address)!.commission.commission_rates.rate
276+
}))
277+
)
278+
279+
const { totalAmount, weightedSum } = pipe(
280+
validDelegations,
281+
Array.reduce(
282+
{ totalAmount: BigDecimal.fromBigInt(0n), weightedSum: BigDecimal.fromBigInt(0n) },
283+
(acc, { amount, commission }) => ({
284+
totalAmount: BigDecimal.sum(acc.totalAmount, amount),
285+
weightedSum: BigDecimal.sum(acc.weightedSum, BigDecimal.multiply(amount, commission))
286+
})
287+
)
199288
)
200289

290+
const weightedAverageCommission = BigDecimal.isZero(totalAmount)
291+
? BigDecimal.fromBigInt(0n)
292+
: yield* BigDecimal.divide(weightedSum, totalAmount).pipe(
293+
Effect.mapError(() =>
294+
new IncentiveError({
295+
message: "Could not calculate weighted average commission",
296+
})
297+
),
298+
)
299+
300+
const validatorCommissionAmount = BigDecimal.multiply(incentiveAfterTax, weightedAverageCommission)
301+
const incentiveAfterCommission = BigDecimal.subtract(incentiveAfterTax, validatorCommissionAmount)
302+
201303
const bondedRatio = yield* BigDecimal.divide(bondedTokens, totalSupply).pipe(
202304
Effect.mapError(() =>
203305
new IncentiveError({
@@ -208,10 +310,14 @@ export const calculateIncentive: Effect.Effect<
208310

209311
return {
210312
rates: {
211-
yearly: incentiveAfterTax,
313+
yearly: incentiveAfterCommission,
212314
},
213315
incentiveNominal,
214316
incentiveAfterTax,
317+
incentiveAfterCommission,
318+
communityTaxAmount,
319+
validatorCommissionAmount,
320+
weightedAverageCommission,
215321
inflation,
216322
totalSupply,
217323
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)