Skip to content

Commit

Permalink
feat: added scaffolding to calculate deriavtive APY on back end
Browse files Browse the repository at this point in the history
  • Loading branch information
SissonJ committed Mar 27, 2024
1 parent 77c7e0f commit 5319c76
Show file tree
Hide file tree
Showing 11 changed files with 528 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/contracts/definitions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './snip20';
export * from './swap';
export * from './derivativeShd';
export * from './derivativeScrt';
export * from './shadeStaking';
8 changes: 8 additions & 0 deletions src/contracts/definitions/shadeStaking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* message for the getting staking opportunity info from the shade staking contract
*/
const msgQueryShadeStakingOpportunity = () => ({ staking_info: {} });

export {
msgQueryShadeStakingOpportunity,
};
1 change: 1 addition & 0 deletions src/contracts/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './snip20';
export * from './swap';
export * from './derivativeScrt';
export * from './derivativeShd';
export * from './shadeStaking';
86 changes: 86 additions & 0 deletions src/contracts/services/shadeStaking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { getActiveQueryClient$ } from '~/client';
import { sendSecretClientContractQuery$ } from '~/client/services/clientServices';
import {
switchMap,
first,
map,
lastValueFrom,
} from 'rxjs';
import { convertCoinFromUDenom } from '~/lib/utils';
import { msgQueryShadeStakingOpportunity } from '~/contracts/definitions/shadeStaking';
import {
StakingInfoServiceResponse,
StakingRewardPoolServiceModel,
StakingInfoServiceModel,
} from '~/types/contracts/shadeStaking/index';

function parseStakingOpportunity(data: StakingInfoServiceResponse): StakingInfoServiceModel {
const stakeTokenAddress = data.staking_info.info.stake_token;
const totalStakedRaw = data.staking_info.info.total_staked;
const unbondingPeriod = Number(data.staking_info.info.unbond_period);
const rewardPools: StakingRewardPoolServiceModel[] = data.staking_info.info.reward_pools
.map((reward) => ({
id: reward.id,
amountRaw: reward.amount,
startDate: new Date(Number(reward.start) * 1000),
endDate: new Date(Number(reward.end) * 1000),
tokenAddress: reward.token.address,
// data returned from the contract in normalized form with
// 18 decimals, in addition to any decimals on the individual token
rateRaw: convertCoinFromUDenom(reward.rate, 18).toString(),
}));
return {
stakeTokenAddress,
totalStakedRaw,
unbondingPeriod,
rewardPools,
};
}

/**
* query the staking info from the shade staking contract
*/
const queryShadeStakingOpportunity$ = ({
shadeStakingContractAddress,
shadeStakingCodeHash,
lcdEndpoint,
chainId,
}: {
shadeStakingContractAddress: string,
shadeStakingCodeHash?: string,
lcdEndpoint?: string,
chainId?: string,
}) => getActiveQueryClient$(lcdEndpoint, chainId).pipe(
switchMap(({ client }) => sendSecretClientContractQuery$({
queryMsg: msgQueryShadeStakingOpportunity(),
client,
contractAddress: shadeStakingContractAddress,
codeHash: shadeStakingCodeHash,
})),
map((response) => parseStakingOpportunity(response as StakingInfoServiceResponse)),
first(),
);

async function queryShadeStakingOpportunity({
shadeStakingContractAddress,
shadeStakingCodeHash,
lcdEndpoint,
chainId,
}: {
shadeStakingContractAddress: string,
shadeStakingCodeHash?: string,
lcdEndpoint?: string,
chainId?: string,
}) {
return lastValueFrom(queryShadeStakingOpportunity$({
shadeStakingContractAddress,
shadeStakingCodeHash,
lcdEndpoint,
chainId,
}));
}

export {
queryShadeStakingOpportunity$,
queryShadeStakingOpportunity,
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export {
convertCoinToUDenom,
} from '~/lib/utils';
export * from './types';
export * from '~/lib/apy/derivativeShd';
export * from '~/lib/apy/derivativeScrt';
112 changes: 112 additions & 0 deletions src/lib/apy/derivativeScrt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
SecretChainDataQueryModel,
SecretQueryOptions,
ValidatorRate,
} from '~/types/apy';
import { lastValueFrom, map } from 'rxjs';
import { DerivativeScrtValidator } from '~/types/contracts/derivativeScrt/model';
import { secretChainQueries$ } from './secretQueries';
import { convertCoinFromUDenom } from '../utils';

const SECRET_DECIMALS = 6;

/**
* Get single validator commission rate from a list of all validators
* @example Example usage of getValidatorCommission
* // returns 0.05 if commission was 5%
*/
function getValidatorCommission(
validatorAddress: string,
validatorList: ValidatorRate[],
):number {
const result = validatorList.filter((
validator,
) => validator.validatorAddress === validatorAddress);
if (result[0]) {
const commission = Number(result[0].ratePercent);
return commission;
}
throw new Error(`Error: validator address ${validatorAddress} not found in list`);
}

/**
* Calculate an Aggregate Staking Rewards Return Rate based on commission rates and weights assigned
* to each validator and their commission rates.
*/
function calcAggregateAPR({
networkValidatorList,
validatorSet,
inflationRate,
totalScrtStaked,
totalScrtSupply,
foundationTax,
communityTax,
}:
{
networkValidatorList:ValidatorRate[],
validatorSet: DerivativeScrtValidator[],
inflationRate:number,
totalScrtStaked:number,
totalScrtSupply:number,
foundationTax:number,
communityTax:number,
}) {
let aggregateApr = 0;
validatorSet.forEach((validator) => {
// Get commission rate for a single validator
const commissionRate = getValidatorCommission(validator.validatorAddress, networkValidatorList);
// Calculate APR of a single validator
const apr = (inflationRate / (totalScrtStaked / totalScrtSupply))
* (1 - foundationTax - communityTax)
* (1 - commissionRate);

// Calculate weighted average APR
aggregateApr += (apr * validator.weight) / 100;
});
return aggregateApr;
}

/**
* Convert APR to APY (Annual Percentage Yield)
* @param {number} periodRate - compounding times per year,
* ex. for daily compounding periodRate=365
* @param {number} apr - Annual Percentage Rate
*/
const calcAPY = (periodRate:number, apr:number):number => (1 + apr / periodRate) ** periodRate - 1;

/**
* Will calculate APY for the stkd secret derivative contract
*/
function calculateDerivativeScrtApy$(lcdEndpoint: string) {
const queries = Object.values(SecretQueryOptions);
return secretChainQueries$(lcdEndpoint, queries).pipe(
map((response: SecretChainDataQueryModel) => {
const apr = calcAggregateAPR({
networkValidatorList: response.secretValidators!,
validatorSet: [], // TODO
inflationRate: response.secretInflationPercent!,
totalScrtStaked: convertCoinFromUDenom(
response.secretTotalStakedRaw!,
SECRET_DECIMALS,
).toNumber(),
totalScrtSupply: convertCoinFromUDenom(
response.secretTotalSupplyRaw!,
SECRET_DECIMALS,
).toNumber(),
foundationTax: response.secretTaxes!.foundationTaxPercent,
communityTax: response.secretTaxes!.communityTaxPercent,
});
return calcAPY(365, apr);
}),
);
}

function calculateDerivativeScrtApy(lcdEndpoint: string) {
return lastValueFrom(calculateDerivativeScrtApy$(lcdEndpoint));
}

export {
calcAPY,
calculateDerivativeScrtApy$,
calculateDerivativeScrtApy,
};
84 changes: 84 additions & 0 deletions src/lib/apy/derivativeShd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import BigNumber from 'node_modules/bignumber.js/bignumber';
import { queryShadeStakingOpportunity$ } from '~/contracts/services/shadeStaking';
import { map } from 'rxjs';
import { StakingInfoServiceModel } from '~/types/contracts/shadeStaking/index';
import { calcAPY } from './derivativeScrt';
import { convertCoinFromUDenom } from '../utils';

/**
* Calculate APY
* Formula is (1+r/n)^n-1
* r = period rate
* n = number of compounding periods
*/
function calculateRewardPoolAPY({
rate,
totalStaked,
price,
decimalPlaces,
}:{
rate: number,
totalStaked: string,
price: string,
decimalPlaces: number,
}) {
// Check that price returned successfully
if (!BigNumber(price).isZero()) {
return BigNumber(0);
}

const SECONDS_PER_YEAR = 31536000;
const rewardsPerYearPerStakedToken = BigNumber(rate).multipliedBy(
SECONDS_PER_YEAR,
).dividedBy(BigNumber(totalStaked));
// period rate = rewardsPerYear* price
const periodRate = rewardsPerYearPerStakedToken.multipliedBy(BigNumber(price));
// divide by stakedPrice to determine a percentage. Units are now ($)/($*day)
const r = periodRate.dividedBy(BigNumber(price));
return BigNumber(calcAPY(365, r.toNumber())).decimalPlaces(decimalPlaces).toNumber();
}

function calculateDerivativeShdApy$({
shadeTokenContractAddress,
shadeStakingContractAddress,
shadeStakingCodeHash,
decimals,
price,
lcdEndpoint,
chainId,
}:{
shadeTokenContractAddress: string,
shadeStakingContractAddress: string,
shadeStakingCodeHash?: string,
decimals: number,
price: string,
lcdEndpoint?: string,
chainId?: string,
}) {
return queryShadeStakingOpportunity$({
shadeStakingContractAddress,
shadeStakingCodeHash,
lcdEndpoint,
chainId,
}).pipe(
map((response: StakingInfoServiceModel) => response.rewardPools.reduce(
(prev, current) => {
if (current.tokenAddress === shadeTokenContractAddress
&& current.endDate.getTime() > Date.now()) {
return prev.plus(calculateRewardPoolAPY({
rate: convertCoinFromUDenom(current.rateRaw, decimals).toNumber(),
totalStaked: response.totalStakedRaw,
price,
decimalPlaces: decimals,
}));
}
return prev;
},
BigNumber(0),
)),
);
}

export {
calculateDerivativeShdApy$,
};
Loading

0 comments on commit 5319c76

Please sign in to comment.