-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added scaffolding to calculate deriavtive APY on back end
- Loading branch information
Showing
11 changed files
with
528 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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$, | ||
}; |
Oops, something went wrong.