diff --git a/.changeset/fifty-shirts-melt.md b/.changeset/fifty-shirts-melt.md new file mode 100644 index 0000000..3096183 --- /dev/null +++ b/.changeset/fifty-shirts-melt.md @@ -0,0 +1,5 @@ +--- +"@shadeprotocol/shadejs": patch +--- + +Batch query can split queries to avoid hitting query gas limits diff --git a/.changeset/healthy-hornets-jam.md b/.changeset/healthy-hornets-jam.md new file mode 100644 index 0000000..b211f7e --- /dev/null +++ b/.changeset/healthy-hornets-jam.md @@ -0,0 +1,5 @@ +--- +"@shadeprotocol/shadejs": minor +--- + +added apy calculations for derivatives and shade staking query interface diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index a1ca801..29802f1 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -28,6 +28,7 @@ export default defineConfig({ { text: 'Swap', link: '/queries/swap' }, { text: 'Oracle', link: '/queries/oracle' }, { text: 'stkd-SCRT', link: '/queries/derivativeScrt' }, + { text: 'Shade Staking', link: '/queries/shadeStaking' }, { text: 'Batch Query', link: '/queries/batch-query' }, { text: 'Snip20', link: '/queries/snip20' }, ] @@ -45,6 +46,7 @@ export default defineConfig({ items: [ { text: 'Swap', link: '/calculations/swap' }, { text: 'Routing', link: '/calculations/routing' }, + { text: 'Apy', link: '/calculations/apy' }, ] }, { diff --git a/docs/calculations/apy.md b/docs/calculations/apy.md new file mode 100644 index 0000000..dc60141 --- /dev/null +++ b/docs/calculations/apy.md @@ -0,0 +1,29 @@ +# APY Calculations + +This page demonstrates how to calculate outputs the shade derivative contracts + +### stkd-SCRT APY +Calculates the apy expected for the stkd-SCRT token +```ts +/** + * Will calculate APY for the stkd secret derivative contract + * + * returns a number that is the decimal form of the percent APY + * @param lcdEndpoint is not optional due to the requirement of the secretChainQueries() function + */ +function calculateDerivativeScrtApy({ + queryRouterContractAddress, + queryRouterCodeHash, + contractAddress, + codeHash, + lcdEndpoint, + chainId, +}: { + queryRouterContractAddress: string, + queryRouterCodeHash?: string, + contractAddress: string, + codeHash: string, + lcdEndpoint: string, + chainId?: string, +}): Promise +``` diff --git a/docs/contracts.md b/docs/contracts.md index c2e5f79..947014d 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -10,6 +10,7 @@ This page contains a list of deployed contracts. | Batch Query Router | secret17gnlxnwux0szd7qhl90ym8lw22qvedjz4v09dm | 72a09535b77b76862f7b568baf1ddbe158a2e4bbd0f0879c69ada9b398e31c1f | | Oracle | secret10n2xl5jmez6r9umtdrth78k0vwmce0l5m9f5dm | 32c4710842b97a526c243a68511b15f58d6e72a388af38a7221ff3244c754e91 | | stkd-scrt | secret1k6u0cy4feepm6pehnz804zmwakuwdapm69tuc4 | f6be719b3c6feb498d3554ca0398eb6b7e7db262acb33f84a8f12106da6bbb09 | +| Shade Staking | secret1y6px5x7jzrk8hyvy67f06ytn8v0jwculypwxws | 2a1ae7fd2be82931cb11d0ce82b2e243507f2006074e2f316da661beb1abe3c3 | ::: tip The ShadeSwap pairs contracts are accessible via the factory registered pairs query. @@ -24,3 +25,4 @@ The ShadeSwap pairs contracts are accessible via the factory registered pairs qu | Batch Query Router | secret10cxkxmspt44mp2wym6dcguk5wqqkm5a9ydw3du | 72a09535b77b76862f7b568baf1ddbe158a2e4bbd0f0879c69ada9b398e31c1f | | Oracle | secret17z47r9u4nqytpdgvewxq4jqd965sfj2wpsnlak | 113c47c016667817b315dde03b4ee9774edf1fb293a7ea3f02d983c6b1fa1cf1 | | stkd-scrt | secret14svk0x3sztxwta9kv9dv6fwzqlc26mmjfyypc2 | 680fbb3c8f8eb1c920da13d857daaedaa46ab8f9a8e26e892bb18a16985ec29e | +| Shade Staking | secret1f9ph34ydnnqg0rzs6h39ucswspllutp9c4ch4k | 5e1e1b0c2a8e2114d29725002be7206598ec68f4bdb28718b082fd84748d416f | diff --git a/docs/queries/batch-query.md b/docs/queries/batch-query.md index 68751c0..bf750f6 100644 --- a/docs/queries/batch-query.md +++ b/docs/queries/batch-query.md @@ -4,23 +4,10 @@ This page demonstrates how to query using a smart contract batch query router. T The batch query function is generalized to work with any contract queries. Miscellaneous ShadeJS services already implement the batch query router, for example the Pairs Info Query +By default, the batch query will process all queries in a single +thread, i.e. using a single node to query. However, there are query +gas limitations controlled by the node provider that will throw errors when batch size is too large. When a batch size is provided as an input to the batch query, it will divide the batch into multi-threaded batches of batches of that size. There are certain batch sizes that have already been tested and are provided for specific ShadeJS services and recommended node settings, however your batch size will vary based on the type of query you are performing and your node provider settings. -## Performance -All of the following results are run with 500 total queries - -| Batch Size | RPC Queries | Time | Success | -|------------|-------------|-------|---------| -| 1 | 500 | 5.56s | 26.6% | -| 5 | 100 | 6.51s | 100% | -| 10 | 50 | 9.24s | 100% | -| 25 | 20 | 7.27s | 100% | -| 50 | 10 | 8.78s | 100% | -| 100 | 5 | 7.72s | 100% | - -::: warning -This testing was completed with a limited sample size and optimal batch size has -not yet determined. There is also an unknown variable of the quality of the infrastructure you would be using to perform these queries. It is encouraged to perform your own optimization testing when working with a batch query router. -::: ## Batch Query **input** @@ -41,12 +28,14 @@ async function batchQuery({ lcdEndpoint, chainId, queries, + batchSize, // defaults to all queries in single batch }:{ contractAddress: string, codeHash?: string, lcdEndpoint?: string, chainId?: string, - queries: BatchQueryParams[] + queries: BatchQueryParams[], + batchSize?: number, }): Promise ``` diff --git a/docs/queries/shadeStaking.md b/docs/queries/shadeStaking.md new file mode 100644 index 0000000..557e9ff --- /dev/null +++ b/docs/queries/shadeStaking.md @@ -0,0 +1,46 @@ +# Shade Staking Examples + +This page demonstrates how to query the shade staking contracts + +## Staking Info + +**input** + +```ts +/** + * query the staking info from the shade staking contract + */ +async function queryShadeStakingOpportunity({ + shadeStakingContractAddress, + shadeStakingCodeHash, + lcdEndpoint, + chainId, +}: { + shadeStakingContractAddress: string, + shadeStakingCodeHash?: string, + lcdEndpoint?: string, + chainId?: string, +}): Promise +``` + +**output** + +```ts +type StakingInfoServiceModel = { + stakeTokenAddress: string, + totalStakedRaw: string, + unbondingPeriod: number, + rewardPools: StakingRewardPoolServiceModel[], +} + +// type references below + +type StakingRewardPoolServiceModel = { + id: string, + amountRaw: string, + startDate: Date, + endDate: Date, + tokenAddress: string, + rateRaw: string, +} +``` diff --git a/src/contracts/definitions/index.ts b/src/contracts/definitions/index.ts index 58ea016..933e643 100644 --- a/src/contracts/definitions/index.ts +++ b/src/contracts/definitions/index.ts @@ -4,3 +4,4 @@ export * from './snip20'; export * from './swap'; export * from './derivativeShd'; export * from './derivativeScrt'; +export * from './shadeStaking'; diff --git a/src/contracts/definitions/shadeStaking.test.ts b/src/contracts/definitions/shadeStaking.test.ts new file mode 100644 index 0000000..37e3f2f --- /dev/null +++ b/src/contracts/definitions/shadeStaking.test.ts @@ -0,0 +1,10 @@ +import { + test, + expect, +} from 'vitest'; +import { msgQueryShadeStakingOpportunity } from './shadeStaking'; + +test('it tests the form of the query staking info msg', () => { + const output = { staking_info: {} }; + expect(msgQueryShadeStakingOpportunity()).toStrictEqual(output); +}); diff --git a/src/contracts/definitions/shadeStaking.ts b/src/contracts/definitions/shadeStaking.ts new file mode 100644 index 0000000..ab39ea3 --- /dev/null +++ b/src/contracts/definitions/shadeStaking.ts @@ -0,0 +1,8 @@ +/** + * message for the getting staking opportunity info from the shade staking contract + */ +const msgQueryShadeStakingOpportunity = () => ({ staking_info: {} }); + +export { + msgQueryShadeStakingOpportunity, +}; diff --git a/src/contracts/services/batchQuery.test.ts b/src/contracts/services/batchQuery.test.ts index 8d0754d..7bf3209 100644 --- a/src/contracts/services/batchQuery.test.ts +++ b/src/contracts/services/batchQuery.test.ts @@ -4,6 +4,7 @@ import { vi, beforeAll, afterAll, + afterEach, } from 'vitest'; import { of } from 'rxjs'; import batchPairConfigResponse from '~/test/mocks/batchQuery/batchPairConfigResponse.json'; @@ -12,10 +13,13 @@ import { BatchQueryParams } from '~/types/contracts/batchQuery/model'; import { msgBatchQuery } from '~/contracts/definitions/batchQuery'; import batchPricesWithError from '~/test/mocks/batchQuery/batchIndividualPricesWithErrorResponse.json'; import { batchPricesWithErrorParsed } from '~/test/mocks/batchQuery/batchPricesWithErrorParsed'; +import { SecretNetworkClient } from 'secretjs'; import { parseBatchQuery, batchQuery$, batchQuery, + batchQuerySingleBatch$, + divideSingleBatchIntoArrayOfMultipleBatches, } from './batchQuery'; const sendSecretClientContractQuery$ = vi.hoisted(() => vi.fn()); @@ -38,6 +42,10 @@ afterAll(() => { vi.clearAllMocks(); }); +afterEach(() => { + vi.clearAllMocks(); +}); + test('it can parse the batch query success response', () => { expect(parseBatchQuery( batchPairConfigResponse, @@ -50,13 +58,41 @@ test('it can parse the batch query mixed succes/error response', () => { )).toStrictEqual(batchPricesWithErrorParsed); }); -test('it can call the batch query service', async () => { +test('it can call the single batch query service', async () => { + const input = { + contractAddress: 'CONTRACT_ADDRESS', + codeHash: 'CODE_HASH', + queries: ['BATCH_QUERY' as unknown as BatchQueryParams], + client: 'SECRET_CLIENT' as unknown as SecretNetworkClient, + }; + // observables function + sendSecretClientContractQuery$.mockReturnValueOnce(of(batchPairConfigResponse)); + + let output; + batchQuerySingleBatch$(input).subscribe({ + next: (response) => { + output = response; + }, + }); + + expect(msgBatchQuery).toHaveBeenNthCalledWith(1, input.queries); + expect(output).toStrictEqual(batchPairConfigParsed); + + // async/await function + sendSecretClientContractQuery$.mockReturnValueOnce(of(batchPairConfigResponse)); + const response = await batchQuery(input); + expect(msgBatchQuery).toHaveBeenNthCalledWith(2, input.queries); + expect(response).toStrictEqual(batchPairConfigParsed); +}); + +test('it can call the multi-batch query service on a single batch', async () => { const input = { contractAddress: 'CONTRACT_ADDRESS', codeHash: 'CODE_HASH', lcdEndpoint: 'LCD_ENDPOINT', chainId: 'CHAIN_ID', queries: ['BATCH_QUERY' as unknown as BatchQueryParams], + // no batch param passed in, so it will process as a single batch }; // observables function sendSecretClientContractQuery$.mockReturnValueOnce(of(batchPairConfigResponse)); @@ -77,3 +113,89 @@ test('it can call the batch query service', async () => { expect(msgBatchQuery).toHaveBeenNthCalledWith(2, input.queries); expect(response).toStrictEqual(batchPairConfigParsed); }); + +test('it can call the multi-batch query service for multiple batches', async () => { + const input = { + contractAddress: 'CONTRACT_ADDRESS', + codeHash: 'CODE_HASH', + lcdEndpoint: 'LCD_ENDPOINT', + chainId: 'CHAIN_ID', + queries: [ + 'BATCH_QUERY_1' as unknown as BatchQueryParams, + 'BATCH_QUERY_2' as unknown as BatchQueryParams, + ], + batchSize: 1, + }; + + // combined array of two of the outputs + const combinedOutput = batchPairConfigParsed.concat(batchPairConfigParsed); + + // observables function + // provide two mocks of the same data + sendSecretClientContractQuery$ + .mockReturnValueOnce(of(batchPairConfigResponse)) + .mockReturnValueOnce(of(batchPairConfigResponse)); + + let output; + batchQuery$(input).subscribe({ + next: (response) => { + output = response; + }, + }); + + expect(msgBatchQuery).toHaveBeenNthCalledWith(1, [input.queries[0]]); + expect(msgBatchQuery).toHaveBeenNthCalledWith(2, [input.queries[1]]); + + expect(output).toStrictEqual(combinedOutput); + + // async/await function + sendSecretClientContractQuery$ + .mockReturnValueOnce(of(batchPairConfigResponse)) + .mockReturnValueOnce(of(batchPairConfigResponse)); + + const response = await batchQuery(input); + expect(msgBatchQuery).toHaveBeenNthCalledWith(3, [input.queries[0]]); + expect(msgBatchQuery).toHaveBeenNthCalledWith(4, [input.queries[1]]); + expect(response).toStrictEqual(combinedOutput); +}); + +test('it can divide a batch of queries into an array of multiple batches', () => { + const input1 = [ + 1 as unknown as BatchQueryParams, + 2 as unknown as BatchQueryParams, + 3 as unknown as BatchQueryParams, + 4 as unknown as BatchQueryParams, + 5 as unknown as BatchQueryParams, + 6 as unknown as BatchQueryParams, + 7 as unknown as BatchQueryParams, + 8 as unknown as BatchQueryParams, + ]; + + expect(divideSingleBatchIntoArrayOfMultipleBatches(input1, 2)).toStrictEqual([ + [1, 2], + [3, 4], + [5, 6], + [7, 8], + ]); + + expect(divideSingleBatchIntoArrayOfMultipleBatches(input1, 3)).toStrictEqual([ + [1, 2, 3], + [4, 5, 6], + [7, 8], + ]); + + expect(divideSingleBatchIntoArrayOfMultipleBatches(input1, 1)).toStrictEqual([ + [1], + [2], + [3], + [4], + [5], + [6], + [7], + [8], + ]); + + expect(divideSingleBatchIntoArrayOfMultipleBatches(input1, 10)).toStrictEqual([ + [1, 2, 3, 4, 5, 6, 7, 8], + ]); +}); diff --git a/src/contracts/services/batchQuery.ts b/src/contracts/services/batchQuery.ts index 70cab2e..bfeaa8e 100644 --- a/src/contracts/services/batchQuery.ts +++ b/src/contracts/services/batchQuery.ts @@ -3,6 +3,10 @@ import { first, map, lastValueFrom, + forkJoin, + concatAll, + reduce, + catchError, } from 'rxjs'; import { sendSecretClientContractQuery$ } from '~/client/services/clientServices'; import { getActiveQueryClient$ } from '~/client'; @@ -15,6 +19,7 @@ import { } from '~/types/contracts/batchQuery/model'; import { BatchQueryResponse } from '~/types/contracts/batchQuery/response'; import { decodeB64ToJson } from '~/lib/utils'; +import { SecretNetworkClient } from 'secretjs'; /** * a parses the batch query response into a usable data model @@ -42,34 +47,94 @@ function parseBatchQuery(response: BatchQueryResponse): BatchQueryParsedResponse }); } +// Function to divide an array of queries into batches of arrays +function divideSingleBatchIntoArrayOfMultipleBatches(array: BatchQueryParams[], batchSize: number) { + const batches = []; + for (let i = 0; i < array.length; i += batchSize) { + batches.push(array.slice(i, i + batchSize)); + } + return batches; +} + /** * batch query of multiple contracts/message at a time */ +const batchQuerySingleBatch$ = ({ + contractAddress, + codeHash, + queries, + client, +}:{ + contractAddress: string, + codeHash?: string, + queries: BatchQueryParams[], + client: SecretNetworkClient +}) => sendSecretClientContractQuery$({ + queryMsg: msgBatchQuery(queries), + client, + contractAddress, + codeHash, +}).pipe( + map((response) => parseBatchQuery(response as BatchQueryResponse)), + first(), + catchError((err) => { + if (err.message.includes('{wasm contract}')) { + throw new Error('{wasm contract} error that typically occurs when batch size is too large and node gas query limits are exceeded. Consider reducing the batch size.'); + } else { + throw new Error(err); + } + }), +); + +/** + * batch query of multiple contracts/message at a time + * @param batchSize defaults to processing all queries in a single batch + * when the batchSize is not passed in. + */ const batchQuery$ = ({ contractAddress, codeHash, lcdEndpoint, chainId, queries, + batchSize, }:{ contractAddress: string, codeHash?: string, lcdEndpoint?: string, chainId?: string, - queries: BatchQueryParams[] -}) => getActiveQueryClient$(lcdEndpoint, chainId).pipe( - switchMap(({ client }) => sendSecretClientContractQuery$({ - queryMsg: msgBatchQuery(queries), - client, - contractAddress, - codeHash, - })), - map((response) => parseBatchQuery(response as BatchQueryResponse)), - first(), -); + queries: BatchQueryParams[], + batchSize?: number, +}) => { + // if batch size is passed in, convert single batch into multiple batches, + // otherwise process all data in a single batch + const batches = batchSize + ? divideSingleBatchIntoArrayOfMultipleBatches(queries, batchSize) + : [queries]; // array of arrays required for the forkJoin + + return getActiveQueryClient$(lcdEndpoint, chainId).pipe( + switchMap(({ client }) => forkJoin( + batches.map((batch) => batchQuerySingleBatch$({ + contractAddress, + codeHash, + queries: batch, + client, + })), + ).pipe( + concatAll(), + reduce(( + acc: BatchQueryParsedResponse, + curr: BatchQueryParsedResponse, + ) => acc.concat(curr), []), // Flatten nested arrays into a single array + first(), + )), + ); +}; /** * batch query of multiple contracts/message at a time + * @param batchSize defaults to processing all queries in a single batch + * when the batchSize is not passed in. */ async function batchQuery({ contractAddress, @@ -77,12 +142,14 @@ async function batchQuery({ lcdEndpoint, chainId, queries, + batchSize, }:{ contractAddress: string, codeHash?: string, lcdEndpoint?: string, chainId?: string, - queries: BatchQueryParams[] + queries: BatchQueryParams[], + batchSize?: number, }) { return lastValueFrom(batchQuery$({ contractAddress, @@ -90,6 +157,7 @@ async function batchQuery({ lcdEndpoint, chainId, queries, + batchSize, })); } @@ -97,4 +165,6 @@ export { parseBatchQuery, batchQuery$, batchQuery, + batchQuerySingleBatch$, + divideSingleBatchIntoArrayOfMultipleBatches, }; diff --git a/src/contracts/services/config.ts b/src/contracts/services/config.ts new file mode 100644 index 0000000..453e530 --- /dev/null +++ b/src/contracts/services/config.ts @@ -0,0 +1,8 @@ +const SERVICE_BATCH_SIZE = { + PAIR_INFO: 60, + PAIR_CONFIG: 60, +}; + +export { + SERVICE_BATCH_SIZE, +}; diff --git a/src/contracts/services/derivativeShd.ts b/src/contracts/services/derivativeShd.ts index 4ca7ba0..5abd0fb 100644 --- a/src/contracts/services/derivativeShd.ts +++ b/src/contracts/services/derivativeShd.ts @@ -15,8 +15,8 @@ import { import { convertCoinFromUDenom } from '~/lib/utils'; import { msgQueryShdDerivativeStakingInfo } from '~/contracts/definitions/derivativeShd'; -// Contract returns price as a rate of dSHD/SHD with 6 decimals -const DERIVATE_PRICE_DECIMALS = 6; +// Contract returns price as a rate of dSHD/SHD with 8 decimals +const DERIVATE_PRICE_DECIMALS = 8; /** * Parses the staking info query into a cleaner data model diff --git a/src/contracts/services/index.ts b/src/contracts/services/index.ts index b74164d..b614292 100644 --- a/src/contracts/services/index.ts +++ b/src/contracts/services/index.ts @@ -4,3 +4,4 @@ export * from './snip20'; export * from './swap'; export * from './derivativeScrt'; export * from './derivativeShd'; +export * from './shadeStaking'; diff --git a/src/contracts/services/shadeStaking.test.ts b/src/contracts/services/shadeStaking.test.ts new file mode 100644 index 0000000..a949f56 --- /dev/null +++ b/src/contracts/services/shadeStaking.test.ts @@ -0,0 +1,78 @@ +import { + test, + expect, + vi, + beforeAll, +} from 'vitest'; +import { of } from 'rxjs'; +import stakingOpportunityResponse from '~/test/mocks/shadeStaking/stakingOpportunityResponse.json'; +import { stakingOpportunityResponseParsed } from '~/test/mocks/shadeStaking/response'; +import { + parseStakingOpportunity, + queryShadeStakingOpportunity, + queryShadeStakingOpportunity$, +} from '~/contracts/services/shadeStaking'; +import { StakingInfoServiceResponse } from '~/types/contracts/shadeStaking/index'; + +const sendSecretClientContractQuery$ = vi.hoisted(() => vi.fn()); + +beforeAll(() => { + vi.mock('~/contracts/definitions/shadeStaking', () => ({ + msgQueryShadeStakingOpportunity: vi.fn(() => 'STAKING_INFO_MSG'), + })); + + vi.mock('~/client/index', () => ({ + getActiveQueryClient$: vi.fn(() => of({ client: 'CLIENT' })), + })); + + vi.mock('~/client/services/clientServices', () => ({ + sendSecretClientContractQuery$, + })); +}); + +test('it can parse the shade staking info', () => { + expect(parseStakingOpportunity( + stakingOpportunityResponse as StakingInfoServiceResponse, + )).toStrictEqual(stakingOpportunityResponseParsed); +}); + +test('it can call the query shade staking info service', async () => { + // observables function + sendSecretClientContractQuery$.mockReturnValueOnce(of(stakingOpportunityResponse)); + + const input = { + shadeStakingContractAddress: 'CONTRACT_ADDRESS', + shadeStakingCodeHash: 'CODE_HASH', + lcdEndpoint: 'LCD_ENDPOINT', + chainId: 'CHAIN_ID', + }; + + let output; + queryShadeStakingOpportunity$(input).subscribe({ + next: (response) => { + output = response; + }, + }); + + expect(sendSecretClientContractQuery$).toHaveBeenCalledWith({ + queryMsg: 'STAKING_INFO_MSG', + client: 'CLIENT', + contractAddress: input.shadeStakingContractAddress, + codeHash: input.shadeStakingCodeHash, + }); + + expect(output).toStrictEqual(stakingOpportunityResponseParsed); + + // async/await function + sendSecretClientContractQuery$.mockReturnValueOnce(of(stakingOpportunityResponse)); + const output2 = await queryShadeStakingOpportunity(input); + + expect(sendSecretClientContractQuery$).toHaveBeenCalledWith({ + queryMsg: 'STAKING_INFO_MSG', + client: 'CLIENT', + contractAddress: input.shadeStakingContractAddress, + codeHash: input.shadeStakingCodeHash, + }); + + expect(output2).toStrictEqual(stakingOpportunityResponseParsed); +}); diff --git a/src/contracts/services/shadeStaking.ts b/src/contracts/services/shadeStaking.ts new file mode 100644 index 0000000..27695bf --- /dev/null +++ b/src/contracts/services/shadeStaking.ts @@ -0,0 +1,95 @@ +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'; + +// data returned from the contract in normalized form with +// 18 decimals, in addition to any decimals on the individual token +const NORMALIZATION_FACTOR = 18; + +/** + * parses the response from the shade staking contract into a model + */ +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, + rateRaw: convertCoinFromUDenom(reward.rate, NORMALIZATION_FACTOR).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(), +); + +/** + * query the staking info from the shade staking contract + */ +async function queryShadeStakingOpportunity({ + shadeStakingContractAddress, + shadeStakingCodeHash, + lcdEndpoint, + chainId, +}: { + shadeStakingContractAddress: string, + shadeStakingCodeHash?: string, + lcdEndpoint?: string, + chainId?: string, +}) { + return lastValueFrom(queryShadeStakingOpportunity$({ + shadeStakingContractAddress, + shadeStakingCodeHash, + lcdEndpoint, + chainId, + })); +} + +export { + parseStakingOpportunity, + queryShadeStakingOpportunity$, + queryShadeStakingOpportunity, +}; diff --git a/src/contracts/services/swap.test.ts b/src/contracts/services/swap.test.ts index 91a0821..a8f0b00 100644 --- a/src/contracts/services/swap.test.ts +++ b/src/contracts/services/swap.test.ts @@ -285,6 +285,7 @@ test('it can call the batch pairs info query service', async () => { }, queryMsg: 'PAIR_INFO_MSG', }], + batchSize: 60, }); expect(output).toStrictEqual(pairsInfoParsed); @@ -305,6 +306,7 @@ test('it can call the batch pairs info query service', async () => { }, queryMsg: 'PAIR_INFO_MSG', }], + batchSize: 60, }); expect(response).toStrictEqual(pairsInfoParsed); }); @@ -340,7 +342,7 @@ test('it can call the batch staking info query service', async () => { address: input.stakingContracts[0].address, codeHash: input.stakingContracts[0].codeHash, }, - queryMsg: 'PAIR_INFO_MSG', + queryMsg: 'STAKING_CONFIG_MSG', }], }); @@ -400,6 +402,7 @@ test('it can call the batch pair config query service', async () => { }, queryMsg: 'PAIR_CONFIG_MSG', }], + batchSize: 60, }); expect(output).toStrictEqual(batchPairsConfigParsed); @@ -420,6 +423,7 @@ test('it can call the batch pair config query service', async () => { }, queryMsg: 'PAIR_CONFIG_MSG', }], + batchSize: 60, }); expect(response).toStrictEqual(batchPairsConfigParsed); }); diff --git a/src/contracts/services/swap.ts b/src/contracts/services/swap.ts index c3a62e1..761e158 100644 --- a/src/contracts/services/swap.ts +++ b/src/contracts/services/swap.ts @@ -39,6 +39,7 @@ import { import { TxResponse } from 'secretjs'; import { Attribute } from 'secretjs/dist/protobuf/cosmos/base/abci/v1beta1/abci'; import { batchQuery$ } from './batchQuery'; +import { SERVICE_BATCH_SIZE } from './config'; /** * parses the factory config to a usable data model @@ -509,12 +510,14 @@ function batchQueryPairsInfo$({ lcdEndpoint, chainId, pairsContracts, + batchSize = SERVICE_BATCH_SIZE.PAIR_INFO, }:{ queryRouterContractAddress: string, queryRouterCodeHash?: string, lcdEndpoint?: string, chainId?: string, - pairsContracts: Contract[] + pairsContracts: Contract[], + batchSize?: number, }) { const queries:BatchQueryParams[] = pairsContracts.map((contract) => ({ id: contract.address, @@ -530,6 +533,7 @@ function batchQueryPairsInfo$({ lcdEndpoint, chainId, queries, + batchSize, }).pipe( map(parseBatchQueryPairInfoResponse), first(), @@ -545,12 +549,14 @@ async function batchQueryPairsInfo({ lcdEndpoint, chainId, pairsContracts, + batchSize, }:{ queryRouterContractAddress: string, queryRouterCodeHash?: string, lcdEndpoint?: string, chainId?: string, - pairsContracts: Contract[] + pairsContracts: Contract[], + batchSize?: number, }) { return lastValueFrom(batchQueryPairsInfo$({ queryRouterContractAddress, @@ -558,6 +564,7 @@ async function batchQueryPairsInfo({ lcdEndpoint, chainId, pairsContracts, + batchSize, })); } @@ -570,12 +577,14 @@ function batchQueryPairsConfig$({ lcdEndpoint, chainId, pairsContracts, + batchSize = SERVICE_BATCH_SIZE.PAIR_CONFIG, }:{ queryRouterContractAddress: string, queryRouterCodeHash?: string, lcdEndpoint?: string, chainId?: string, - pairsContracts: Contract[] + pairsContracts: Contract[], + batchSize?: number, }) { const queries:BatchQueryParams[] = pairsContracts.map((contract) => ({ id: contract.address, @@ -591,6 +600,7 @@ function batchQueryPairsConfig$({ lcdEndpoint, chainId, queries, + batchSize, }).pipe( map(parseBatchQueryPairConfigResponse), first(), @@ -606,12 +616,14 @@ async function batchQueryPairsConfig({ lcdEndpoint, chainId, pairsContracts, + batchSize, }:{ queryRouterContractAddress: string, queryRouterCodeHash?: string, lcdEndpoint?: string, chainId?: string, - pairsContracts: Contract[] + pairsContracts: Contract[], + batchSize?: number, }) { return lastValueFrom(batchQueryPairsConfig$({ queryRouterContractAddress, @@ -619,6 +631,7 @@ async function batchQueryPairsConfig({ lcdEndpoint, chainId, pairsContracts, + batchSize, })); } diff --git a/src/index.ts b/src/index.ts index 4f0131e..8305118 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,3 +9,5 @@ export { convertCoinToUDenom, } from '~/lib/utils'; export * from './types'; +export * from '~/lib/apy/derivativeShd'; +export * from '~/lib/apy/derivativeScrt'; diff --git a/src/lib/apy/derivativeScrt.test.ts b/src/lib/apy/derivativeScrt.test.ts new file mode 100644 index 0000000..571f578 --- /dev/null +++ b/src/lib/apy/derivativeScrt.test.ts @@ -0,0 +1,67 @@ +import { + test, + expect, + vi, + beforeAll, +} from 'vitest'; +import { of } from 'rxjs'; +import chainQueryParsedResponse from '~/test/mocks/secretChainQueries/chainQueryParsedResponse.json'; +import { queryDerivativeScrtInfo$ } from '~/contracts/services/derivativeScrt'; +import stakingInfoResponseMainnet from '~/test/mocks/derivativeScrt/stakingInfoResponseMainnet.json'; +import { + secretChainQueries$, + SecretQueryOptions, +} from './secretQueries'; +import { + calculateDerivativeScrtApy, + calculateDerivativeScrtApy$, +} from './derivativeScrt'; + +beforeAll(() => { + vi.mock('~/lib/apy/secretQueries', async (importOriginal: any) => ({ + ...(await importOriginal()), + secretChainQueries$: vi.fn(() => of(chainQueryParsedResponse)), + })); + + vi.mock('~/contracts/services/derivativeScrt', () => ({ + queryDerivativeScrtInfo$: vi.fn(() => of(stakingInfoResponseMainnet)), + })); +}); + +test('it can calculate the correct derivative apy', async () => { + const input = { + queryRouterContractAddress: 'MOCK_QUERY_ADDRESS', + queryRouterCodeHash: 'MOCK_QUERY_ROUTER_CODE_HASH', + contractAddress: 'MOCK_STKD_ADDRESS', + codeHash: 'MOCK_STKD_HASH', + lcdEndpoint: 'MOCK_ENDPOINT', + chainId: 'MOCK_CHAIN_ID', + }; + + let output; + calculateDerivativeScrtApy$(input).subscribe({ + next: (response: number) => { + output = response; + }, + }); + + expect(secretChainQueries$).toHaveBeenCalledWith( + input.lcdEndpoint, + Object.values(SecretQueryOptions), + ); + + expect(queryDerivativeScrtInfo$).toHaveBeenCalledWith({ + queryRouterContractAddress: input.queryRouterContractAddress, + queryRouterCodeHash: input.queryRouterCodeHash, + contractAddress: input.contractAddress, + codeHash: input.codeHash, + lcdEndpoint: input.lcdEndpoint, + chainId: input.chainId, + }); + + expect(output).toStrictEqual(0.1641591628625081); + + const output2 = await calculateDerivativeScrtApy(input); + + expect(output2).toStrictEqual(0.1641591628625081); +}); diff --git a/src/lib/apy/derivativeScrt.ts b/src/lib/apy/derivativeScrt.ts new file mode 100644 index 0000000..c034642 --- /dev/null +++ b/src/lib/apy/derivativeScrt.ts @@ -0,0 +1,109 @@ +import { + SecretChainDataQueryModel, +} from '~/types/apy'; +import { forkJoin, lastValueFrom, map } from 'rxjs'; +import { + DerivativeScrtInfo, +} from '~/types/contracts/derivativeScrt/model'; +import { convertCoinFromUDenom } from '~/lib/utils'; +import { queryDerivativeScrtInfo$ } from '~/contracts/services/derivativeScrt'; +import { + secretChainQueries$, + SecretQueryOptions, +} from './secretQueries'; +import { calcAggregateAPR, calcAPY } from './utils'; + +const SECRET_DECIMALS = 6; + +/** + * Will calculate APY for the stkd secret derivative contract + * + * returns a number that is the decimal form of the percent APY + * @param lcdEndpoint is not optional due to the requirement of the secretChainQueries() function + */ +function calculateDerivativeScrtApy$({ + queryRouterContractAddress, + queryRouterCodeHash, + contractAddress, + codeHash, + lcdEndpoint, + chainId, +}: { + queryRouterContractAddress: string, + queryRouterCodeHash?: string, + contractAddress: string, + codeHash: string, + lcdEndpoint: string, + chainId?: string, +}) { + const queries = Object.values(SecretQueryOptions); + return forkJoin({ + chainParameters: secretChainQueries$(lcdEndpoint, queries), + derivativeInfo: queryDerivativeScrtInfo$({ + queryRouterContractAddress, + queryRouterCodeHash, + contractAddress, + codeHash, + lcdEndpoint, + chainId, + }), + }).pipe( + map((response: { + chainParameters: SecretChainDataQueryModel, + derivativeInfo: DerivativeScrtInfo, + }) => { + const apr = calcAggregateAPR({ + networkValidatorList: response.chainParameters.secretValidators, + validatorSet: response.derivativeInfo.validators, + inflationRate: response.chainParameters.secretInflationPercent, + totalScrtStaked: convertCoinFromUDenom( + response.chainParameters.secretTotalStakedRaw, + SECRET_DECIMALS, + ).toNumber(), + totalScrtSupply: convertCoinFromUDenom( + response.chainParameters.secretTotalSupplyRaw, + SECRET_DECIMALS, + ).toNumber(), + foundationTax: response.chainParameters.secretTaxes!.foundationTaxPercent, + communityTax: response.chainParameters.secretTaxes!.communityTaxPercent, + }); + return calcAPY(365, apr); + }), + ); +} + +/** + * Will calculate APY for the stkd secret derivative contract + * + * returns a number that is the decimal form of the percent APY + * @param lcdEndpoint is not optional due to the requirement of the secretChainQueries() function + */ +function calculateDerivativeScrtApy({ + queryRouterContractAddress, + queryRouterCodeHash, + contractAddress, + codeHash, + lcdEndpoint, + chainId, +}: { + queryRouterContractAddress: string, + queryRouterCodeHash?: string, + contractAddress: string, + codeHash: string, + lcdEndpoint: string, + chainId?: string, +}) { + return lastValueFrom(calculateDerivativeScrtApy$({ + queryRouterContractAddress, + queryRouterCodeHash, + contractAddress, + codeHash, + lcdEndpoint, + chainId, + })); +} + +export { + calculateDerivativeScrtApy$, + calculateDerivativeScrtApy, +}; diff --git a/src/lib/apy/derivativeShd.test.ts b/src/lib/apy/derivativeShd.test.ts new file mode 100644 index 0000000..5117739 --- /dev/null +++ b/src/lib/apy/derivativeShd.test.ts @@ -0,0 +1,51 @@ +import { + test, + expect, + vi, + beforeAll, +} from 'vitest'; +import { of } from 'rxjs'; +import { stakingOpportunityResponseParsed } from '~/test/mocks/shadeStaking/response'; +import { queryShadeStakingOpportunity$ } from '~/contracts/services/shadeStaking'; +import { + calculateDerivativeShdApy$, + calculateDerivativeShdApy, +} from './derivativeShd'; + +beforeAll(() => { + vi.setSystemTime(new Date('2024-03-26T18:00:00.000Z')); + vi.mock('~/contracts/services/shadeStaking', () => ({ + queryShadeStakingOpportunity$: vi.fn(() => of(stakingOpportunityResponseParsed)), + })); +}); + +test('it can calculate the correct derivative apy', async () => { + const input = { + shadeTokenContractAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + shadeStakingContractAddress: 'MOCK_STAKING_ADDRESS', + shadeStakingCodeHash: 'MOCK_STAKING_HASH', + price: '7.15', + lcdEndpoint: 'MOCK_ENDPOINT', + chainId: 'MOCK_CHAIN_ID', + }; + + let output; + calculateDerivativeShdApy$(input).subscribe({ + next: (response: any) => { + output = response; + }, + }); + + expect(queryShadeStakingOpportunity$).toHaveBeenCalledWith({ + shadeStakingContractAddress: input.shadeStakingContractAddress, + shadeStakingCodeHash: input.shadeStakingCodeHash, + lcdEndpoint: input.lcdEndpoint, + chainId: input.chainId, + }); + + expect(output).toStrictEqual(0.051269877232584804); + + const output2 = await calculateDerivativeShdApy(input); + + expect(output2).toStrictEqual(0.051269877232584804); +}); diff --git a/src/lib/apy/derivativeShd.ts b/src/lib/apy/derivativeShd.ts new file mode 100644 index 0000000..d139934 --- /dev/null +++ b/src/lib/apy/derivativeShd.ts @@ -0,0 +1,90 @@ +import { queryShadeStakingOpportunity$ } from '~/contracts/services/shadeStaking'; +import { lastValueFrom, map } from 'rxjs'; +import { StakingInfoServiceModel } from '~/types/contracts/shadeStaking/index'; +import { convertCoinFromUDenom } from '~/lib/utils'; +import { calculateRewardPoolAPY } from './utils'; + +const SHADE_DECIMALS = 8; + +/** + * Calculates the dSHD expected APY by querying the staking contract + * TESTNET ONLY NOT READY FOR PRODUCTION + * + * returns a number that is the decimal form of the percent APY + */ +function calculateDerivativeShdApy$({ + shadeTokenContractAddress, + shadeStakingContractAddress, + shadeStakingCodeHash, + price, + lcdEndpoint, + chainId, +}:{ + shadeTokenContractAddress: string, + shadeStakingContractAddress: string, + shadeStakingCodeHash?: string, + price: string, + lcdEndpoint?: string, + chainId?: string, +}) { + return queryShadeStakingOpportunity$({ + shadeStakingContractAddress, + shadeStakingCodeHash, + lcdEndpoint, + chainId, + }).pipe( + map((response: StakingInfoServiceModel) => response.rewardPools.reduce( + (prev, current) => { + // Make sure to check that we're only calculating for shd + if (current.tokenAddress === shadeTokenContractAddress + && current.endDate.getTime() > Date.now()) { + return prev + calculateRewardPoolAPY({ + rate: convertCoinFromUDenom(current.rateRaw, SHADE_DECIMALS).toNumber(), + totalStaked: response.totalStakedRaw, + price, + stakedPrice: price, + decimals: SHADE_DECIMALS, + }); + } + return prev; + }, + 0, + )), + ); +} + +/** + * Calculates the dSHD expected APY by querying the staking contract + * TESTNET ONLY NOT READY FOR PRODUCTION + * + * returns a number that is the decimal form of the percent APY + */ +async function calculateDerivativeShdApy({ + shadeTokenContractAddress, + shadeStakingContractAddress, + shadeStakingCodeHash, + price, + lcdEndpoint, + chainId, +}:{ + shadeTokenContractAddress: string, + shadeStakingContractAddress: string, + shadeStakingCodeHash?: string, + price: string, + lcdEndpoint?: string, + chainId?: string, +}) { + return lastValueFrom(calculateDerivativeShdApy$({ + shadeTokenContractAddress, + shadeStakingContractAddress, + shadeStakingCodeHash, + price, + lcdEndpoint, + chainId, + })); +} + +export { + calculateDerivativeShdApy$, + calculateDerivativeShdApy, +}; diff --git a/src/lib/apy/secretQueries.test.ts b/src/lib/apy/secretQueries.test.ts new file mode 100644 index 0000000..ecc8dc3 --- /dev/null +++ b/src/lib/apy/secretQueries.test.ts @@ -0,0 +1,77 @@ +import { + test, + expect, + vi, + beforeAll, +} from 'vitest'; +import { of } from 'rxjs'; +import { + secretChainQuery$, + secretChainQuery, + secretChainQueries$, + secretChainQueries, + parseSecretQueryResponse, + SecretQueryOptions, +} from './secretQueries'; + +beforeAll(async () => { + vi.mock('~/client/services/createFetch', () => ({ + createFetch: vi.fn(() => of({ response: 'MOCK_API_RESPONSE' })), + })); +}); + +test('it can parse chain queries', () => { + expect(parseSecretQueryResponse( + { result: 10 }, + SecretQueryOptions.INFLATION, + )).toStrictEqual({ secretInflationPercent: 10 }); + + expect(parseSecretQueryResponse( + { amount: { amount: 10 } }, + SecretQueryOptions.TOTAL_SUPPLY, + )).toStrictEqual({ secretTotalSupplyRaw: 10 }); + + expect(parseSecretQueryResponse( + { result: { bonded_tokens: 10 } }, + SecretQueryOptions.TOTAL_STAKED, + )).toStrictEqual({ secretTotalStakedRaw: 10 }); + + expect(parseSecretQueryResponse( + { result: { community_tax: '10', secret_foundation_tax: '11' } }, + SecretQueryOptions.TAXES, + )).toStrictEqual({ secretTaxes: { foundationTaxPercent: 11, communityTaxPercent: 10 } }); + + expect(parseSecretQueryResponse( + { result: [{ commission: { commission_rates: { rate: '10' } }, operator_address: 'MOCK_ADDRESS' }] }, + SecretQueryOptions.VALIDATORS, + )).toStrictEqual({ secretValidators: [{ ratePercent: 10, validatorAddress: 'MOCK_ADDRESS' }] }); + + expect(parseSecretQueryResponse( + 'nonsence', + '/nonsence/api', + )).toStrictEqual('nonsence'); +}); + +test('it can do a single chain query', async () => { + let output; + secretChainQuery$('MOCK_URL', 'MOCK_ENDPOINT').subscribe({ + next: (response) => { + output = response; + }, + }); + expect(output).toStrictEqual({ response: 'MOCK_API_RESPONSE' }); + const output2 = await secretChainQuery('MOCK_URL', 'MOCK_ENDPOINT'); + expect(output2).toStrictEqual({ response: 'MOCK_API_RESPONSE' }); +}); + +test('it can do many chain queries', async () => { + let output; + secretChainQueries$('MOCK_URL', ['MOCK_ENDPOINT']).subscribe({ + next: (response) => { + output = response; + }, + }); + expect(output).toStrictEqual({ response: 'MOCK_API_RESPONSE' }); + const output2 = await secretChainQueries('MOCK_URL', ['MOCK_ENDPOINT']); + expect(output2).toStrictEqual({ response: 'MOCK_API_RESPONSE' }); +}); diff --git a/src/lib/apy/secretQueries.ts b/src/lib/apy/secretQueries.ts new file mode 100644 index 0000000..e40855f --- /dev/null +++ b/src/lib/apy/secretQueries.ts @@ -0,0 +1,148 @@ +import { + Observable, + defer, + forkJoin, + lastValueFrom, + map, +} from 'rxjs'; +import { fromFetch } from 'rxjs/fetch'; +import { createFetch } from '~/client/services/createFetch'; +import { + SecretValidatorItemResponse, +} from '~/types/apy'; + +enum SecretQueryOptions { + INFLATION = '/minting/inflation', + TOTAL_SUPPLY = '/cosmos/bank/v1beta1/supply/uscrt', + TOTAL_STAKED = '/staking/pool', + TAXES = '/distribution/parameters', + VALIDATORS = '/staking/validators', +} + +/** + * Parses the response from the secretChainQuery service + */ +function parseSecretQueryResponse( + response: any, + query: string, +): ResponseType { + switch (query) { + case SecretQueryOptions.INFLATION: + return { + secretInflationPercent: response?.result, + } as ResponseType; + case SecretQueryOptions.TOTAL_SUPPLY: + return { + secretTotalSupplyRaw: response?.amount?.amount, + }as ResponseType; + case SecretQueryOptions.TOTAL_STAKED: + return { + secretTotalStakedRaw: response?.result?.bonded_tokens, + }as ResponseType; + case SecretQueryOptions.TAXES: + if (response.result + && response.result.community_tax + && response.result.secret_foundation_tax + ) { + return { + secretTaxes: { + foundationTaxPercent: Number(response.result.secret_foundation_tax), + communityTaxPercent: Number(response.result.community_tax), + }, + }as ResponseType; + } + return response as ResponseType; + case SecretQueryOptions.VALIDATORS: { + const parsedValidators = response?.result?.map(( + nextValidator: SecretValidatorItemResponse, + ) => ({ + ratePercent: Number(nextValidator.commission.commission_rates.rate), + validatorAddress: nextValidator.operator_address, + })); + return { + secretValidators: parsedValidators, + } as ResponseType; + } + default: + return response as ResponseType; + } +} + +/** + * Executes various chain queries throught the url provided + * + * ResponseType must be a javascript object + * + * @param lcdEndpoint is used here instead of client since URL is a private argument to client + * The client does not support the types of queries specified in SecretQueryOptions + * @param queries is the endpoint you'd like to hit, ex. '/minting/inflation' + */ +function secretChainQuery$(url: string, query: string): Observable { + return createFetch(defer( + () => fromFetch(`${url}${query}`), + )).pipe( + map((response: any) => parseSecretQueryResponse(response, query)), + ); +} + +/** + * Executes various chain queries throught the url provided + * + * ResponseType must be a javascript object + * + * @param lcdEndpoint is used here instead of client since URL is a private argument to client + * The client does not support the types of queries specified in SecretQueryOptions + * @param queries is the endpoint you'd like to hit, ex. '/minting/inflation' + */ +async function secretChainQuery(url: string, query: string): Promise { + return lastValueFrom(secretChainQuery$(url, query)); +} + +/** + * Executes multiple chain queries throught the url provided + * + * ResponseType must be a javascript object + * + * @param lcdEndpoint is used here instead of client since URL is a private argument to client + * The client does not support the types of queries specified in SecretQueryOptions + * @param queries is the endpoint you'd like to hit, ex. ['/minting/inflation'] + */ +function secretChainQueries$( + url: string, + queries: string[], +): Observable { + return forkJoin(queries.map((nextQuery) => secretChainQuery$(url, nextQuery))).pipe( + map((response: ResponseType[]) => response.reduce(( + parsedResonse, + nextResponse, + ) => ({ + ...parsedResonse, + ...nextResponse, + } as ResponseType), {} as ResponseType)), + ); +} + +/** + * Executes multiple chain queries throught the url provided + * + * ResponseType must be a javascript object + * + * @param lcdEndpoint is used here instead of client since URL is a private argument to client + * The client does not support the types of queries specified in SecretQueryOptions + * @param queries is the endpoint you'd like to hit, ex. ['/minting/inflation'] + */ +async function secretChainQueries( + url: string, + queries: string[], +): Promise { + return lastValueFrom(secretChainQueries$(url, queries)); +} + +export { + SecretQueryOptions, + parseSecretQueryResponse, + secretChainQuery$, + secretChainQuery, + secretChainQueries$, + secretChainQueries, +}; diff --git a/src/lib/apy/utils.test.ts b/src/lib/apy/utils.test.ts new file mode 100644 index 0000000..f5d5f51 --- /dev/null +++ b/src/lib/apy/utils.test.ts @@ -0,0 +1,62 @@ +import { + test, + expect, +} from 'vitest'; +import chainQueryParsedResponse from '~/test/mocks/secretChainQueries/chainQueryParsedResponse.json'; +import stakingInfoResponseMainnet from '~/test/mocks/derivativeScrt/stakingInfoResponseMainnet.json'; +import { + getValidatorCommission, + calcValidatorAPR, + calcAggregateAPR, + calcAPY, + calculateRewardPoolAPY, +} from './utils'; + +test('It gets a validator commission', () => { + expect(() => getValidatorCommission('MOCK_VAL_ADDR', [])).toThrowError(/^Error: validator address MOCK_VAL_ADDR not found in list$/); + expect(getValidatorCommission('secretvaloper1rfnmcuwzf3zn7r025j9zr3ncc7mt9ge56l7se7', chainQueryParsedResponse.secretValidators)).toStrictEqual(0.08); +}); + +test('It calculates APR for a given validator', () => { + expect(calcValidatorAPR({ + inflationRate: 100, + totalScrtStaked: 1234567891234, + totalScrtSupply: 5234567687823434, + foundationTax: 0.01, + communityTax: 0.02, + commissionRate: 0.03, + })).toStrictEqual(398941.5869669289); +}); + +test('It can calculate aggregate apr for a list of validators', () => { + expect(calcAggregateAPR({ + networkValidatorList: chainQueryParsedResponse.secretValidators, + validatorSet: stakingInfoResponseMainnet.validators, + inflationRate: 100, + totalScrtStaked: 1234567891234, + totalScrtSupply: 5234567687823434, + foundationTax: 0.01, + communityTax: 0.02, + })).toStrictEqual(390629.40197532275); +}); + +test('It can calculate APY from an APR', () => { + expect(calcAPY(365, 20)).toStrictEqual(285937254.23695487); +}); + +test('It can calculate APY for a shade staking rewards pool', () => { + expect(calculateRewardPoolAPY({ + rate: 1.2, + totalStaked: '1023403432', + price: '1.33', + stakedPrice: '0.0', + decimals: 8, + })).toStrictEqual(0); + expect(calculateRewardPoolAPY({ + rate: 1.2, + totalStaked: '1023403432', + price: '1.33', + stakedPrice: '1.33', + decimals: 8, + })).toStrictEqual(3766803.32592374); +}); diff --git a/src/lib/apy/utils.ts b/src/lib/apy/utils.ts new file mode 100644 index 0000000..d08e483 --- /dev/null +++ b/src/lib/apy/utils.ts @@ -0,0 +1,148 @@ +import { + ValidatorRate, +} from '~/types/apy'; +import { + DerivativeScrtValidator, +} from '~/types/contracts/derivativeScrt/model'; + +/** +* 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 APR of a single validator based off inflation and commission rate + */ +function calcValidatorAPR({ + inflationRate, + totalScrtStaked, + totalScrtSupply, + foundationTax, + communityTax, + commissionRate, +}:{ + inflationRate: number, + totalScrtStaked: number, + totalScrtSupply: number, + foundationTax: number, + communityTax: number, + commissionRate: number, +}) { + return (inflationRate / (totalScrtStaked / totalScrtSupply)) + * (1 - foundationTax - communityTax) + * (1 - commissionRate); +} + +/** +* 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; + // typically total weight will equal 100, but in rare situations the stkd-SCRT contract + // will automatically remove validators from the active set and the total will not add up to 100 + const totalWeight = validatorSet.reduce((acc, validator) => acc + validator.weight, 0); + + validatorSet.forEach((validator) => { + // Get commission rate for a single validator + const commissionRate = getValidatorCommission(validator.validatorAddress, networkValidatorList); + const apr = calcValidatorAPR({ + inflationRate, + totalScrtStaked, + totalScrtSupply, + foundationTax, + communityTax, + commissionRate, + }); + + // Calculate weighted average APR + aggregateApr += (apr * validator.weight) / totalWeight; + }); + 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; + +/** + * Calculate APY for a shadeStaking reward pool + * Formula is (1+r/n)^n-1 + * r = period rate + * n = number of compounding periods + * + * @param rate is the rate of the reward token released normalized to that tokens decimals + * @param totalStaked is the amount of shade that is staked to the pool (in udenom format) + * @param price is the price of the reward token + * @param stakedPrice is the price of the staking token + * @param decimals is the decimals of the reward token + */ +function calculateRewardPoolAPY({ + rate, + totalStaked, + price, + stakedPrice, + decimals, +}:{ + rate: number, + totalStaked: string, + price: string, + stakedPrice: string, + decimals: number, +}) { + // Check that price returned successfully + if (!Number(stakedPrice)) { + return 0; + } + + const SECONDS_PER_YEAR = 31536000; + const rewardsPerYearPerStakedToken = (rate * SECONDS_PER_YEAR) / Number(totalStaked); + // period rate = rewardsPerYear* price + const periodRate = rewardsPerYearPerStakedToken * Number(price); + // divide by stakedPrice to determine a percentage. Units are now ($)/($*day) + const r = periodRate / Number(stakedPrice); + return calcAPY(365, r) * (10 ** decimals); +} + +export { + getValidatorCommission, + calcValidatorAPR, + calcAggregateAPR, + calcAPY, + calculateRewardPoolAPY, +}; diff --git a/src/test/mocks/derivativeScrt/stakingInfoResponseMainnet.json b/src/test/mocks/derivativeScrt/stakingInfoResponseMainnet.json new file mode 100644 index 0000000..014ae6a --- /dev/null +++ b/src/test/mocks/derivativeScrt/stakingInfoResponseMainnet.json @@ -0,0 +1,80 @@ +{ + "validators": [ + { + "validatorAddress": "secretvaloper12ancx2qcgk04rrkexuhjq93etgktq4nn58z9yu", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper16k26akna7h295rfjx3278s7xusnt736vy437y8", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper18acdn4vaxfkqj0ta25u3ulzdyzekrqe5w9q2n4", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper19dw5tuep06ke9smem3df0lqvhpd25ke3we95n3", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1ahawe276d250zpxt0xgpfg63ymmu63a0svuvgw", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1cpp4s3t962tsn737fj4v8jr7net50qgxpn3nap", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1dpajv4su00mzf4r7rhzytc0vs23njkh2wll6lp", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1gkk02na77t83dvmf9vd7lajptejaqkyug62h56", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1jgx4pn3acae9esq5zha5ym3kzhq6x60frjwkrp", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1lyafdwwzwfmjc8zhsew5pdcdu46na6pg09fql4", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1nqxdqq6qhtaj9m2t2mgvzds0xa6fa2g9sk5ymu", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1pujrvwt9xve0u0t472x5wm0lf5r37thakp7pvw", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1q0rth4fu4svxnw63vjd7w74nadzsdp0fmkhj3d", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1qjk5uduu3zg356chmstcp2eqgdn35jp3ztxh4x", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1t5wtcuwjkdct9qkw2h6m48zu2hectpd6ulmekk", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1tums792cvpugaydvqgl7t6r5khfsgh7n78hs5w", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1vzkdmu0sa8gaj686jh5all7hpmmsp8x87vyz8z", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1wg085tj9xtv84q9l2zgdml46n6ju3mryun4fsu", + "weight": 4 + }, + { + "validatorAddress": "secretvaloper1xyvlmw93ytmcju4suuk8tpuhzu6rtnnwqc6a7y", + "weight": 4 + } + ] +} diff --git a/src/test/mocks/derivativeShd/stakingInfoResponseParsed.ts b/src/test/mocks/derivativeShd/stakingInfoResponseParsed.ts index cb6d249..94a14ad 100644 --- a/src/test/mocks/derivativeShd/stakingInfoResponseParsed.ts +++ b/src/test/mocks/derivativeShd/stakingInfoResponseParsed.ts @@ -4,7 +4,7 @@ export const stakingInfoResponseParsed = { availableShd: '0', rewards: '0', totalDerivativeTokenSupply: '0', - price: 100, + price: 1, feeInfo: { stakingFee: 0.01, unbondingFee: 0.01, diff --git a/src/test/mocks/secretChainQueries/chainQueryParsedResponse.json b/src/test/mocks/secretChainQueries/chainQueryParsedResponse.json new file mode 100644 index 0000000..0f494a3 --- /dev/null +++ b/src/test/mocks/secretChainQueries/chainQueryParsedResponse.json @@ -0,0 +1,320 @@ +{ + "secretInflationPercent": 0.09, + "secretTotalSupplyRaw": 292470737038201, + "secretTotalStakedRaw": 161156183048148, + "secretTaxes": { + "foundationTaxPercent": 0, + "communityTaxPercent": 0.02 + }, + "secretValidators": [ + { + "ratePercent": 0.01, + "validatorAddress": "secretvaloper1q0rth4fu4svxnw63vjd7w74nadzsdp0fmkhj3d" + }, + { + "ratePercent": 0.04, + "validatorAddress": "secretvaloper1qjk5uduu3zg356chmstcp2eqgdn35jp3ztxh4x" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1p5l3hyhe45ck4xy6eysujpeskezz2s59vhqzwm" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1pujrvwt9xve0u0t472x5wm0lf5r37thakp7pvw" + }, + { + "ratePercent": 0.03, + "validatorAddress": "secretvaloper1p7hzp7y50l9f7jl3yv6cdd95a3hc2dfrqfncpt" + }, + { + "ratePercent": 0.08, + "validatorAddress": "secretvaloper1rfnmcuwzf3zn7r025j9zr3ncc7mt9ge56l7se7" + }, + { + "ratePercent": 0.09, + "validatorAddress": "secretvaloper1rtx7h36z9u0pd8kt3detrz5mg5ulys5zupfa35" + }, + { + "ratePercent": 0.1, + "validatorAddress": "secretvaloper1y9efhftxlaf30cgzvqp2362sgt233yuw4p9gwa" + }, + { + "ratePercent": 0.1, + "validatorAddress": "secretvaloper1yv9f4tankaktdtf8lq6rjsx9c9rpfptc7kzhz2" + }, + { + "ratePercent": 0.02, + "validatorAddress": "secretvaloper19z74520tdw5gps5sp5tr8y2mxr40artly6rsw3" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper19dw5tuep06ke9smem3df0lqvhpd25ke3we95n3" + }, + { + "ratePercent": 0.07, + "validatorAddress": "secretvaloper193wdevxmace2katdzpdm9l0z6t0zmeht02v95e" + }, + { + "ratePercent": 0.045, + "validatorAddress": "secretvaloper1xyvlmw93ytmcju4suuk8tpuhzu6rtnnwqc6a7y" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1xtg9ypjasxxxjen8k2n5zcxgc73pq7pnvsmm70" + }, + { + "ratePercent": 0, + "validatorAddress": "secretvaloper1xj5ykuzn0mkq9642yxgqmh4ycplzhr2pza25mk" + }, + { + "ratePercent": 0.01, + "validatorAddress": "secretvaloper1x6efs0le7f9f3r4u2rq4jykcjunsq4us99p7xu" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1xmu8meef8ynlsev3a9hpl5wdxhpzzj0efmzpcj" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1x76f2c2cuwa4e3lttjgqeqva0725ftmqvgvfnv" + }, + { + "ratePercent": 0.0949, + "validatorAddress": "secretvaloper182dg8p7nshdjjt5sypcx6hw9p8vlwqpntpm5k6" + }, + { + "ratePercent": 0.03, + "validatorAddress": "secretvaloper18w7rm926ue3nmy8ay58e3lc2nqnttrlhhgpch6" + }, + { + "ratePercent": 0.099, + "validatorAddress": "secretvaloper18acdn4vaxfkqj0ta25u3ulzdyzekrqe5w9q2n4" + }, + { + "ratePercent": 0.099, + "validatorAddress": "secretvaloper18762353s6ulgla3efvf0hpe5rjjxp5ndfvl8z7" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1gkk02na77t83dvmf9vd7lajptejaqkyug62h56" + }, + { + "ratePercent": 0.07, + "validatorAddress": "secretvaloper1ghq67qyjf9exqjw4s3ltg56zf5yceve7s4kj5u" + }, + { + "ratePercent": 0.08, + "validatorAddress": "secretvaloper1glyaxntl2jm3sruafq4rhxfrq4vzrgz0mg9m90" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1f8chr3y3s9h8g4vc5pg8wvzzhfy3hcxm0re5zc" + }, + { + "ratePercent": 0, + "validatorAddress": "secretvaloper12y30xefd0wg53xtyv9lw7mjcdvf8nxgzzva3sr" + }, + { + "ratePercent": 0.1, + "validatorAddress": "secretvaloper12haa5h8wker0x07e6unxgjjvr9wtqn68sptvvh" + }, + { + "ratePercent": 0.1, + "validatorAddress": "secretvaloper126d4dqs02l5xd4e74vkcdspqeeels7vd0hmkt5" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper12ancx2qcgk04rrkexuhjq93etgktq4nn58z9yu" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1t5wtcuwjkdct9qkw2h6m48zu2hectpd6ulmekk" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1tmtcu980raqvypdf0dd6hsgh6qcm7ex7l29u58" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1tums792cvpugaydvqgl7t6r5khfsgh7n78hs5w" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1vp05jj9t0u228j3ph8qav642mh84lp2q6r8vhx" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1vzkdmu0sa8gaj686jh5all7hpmmsp8x87vyz8z" + }, + { + "ratePercent": 0.1, + "validatorAddress": "secretvaloper1vws5gh9pcpd6xeljl9qgu28mkqtvket49aqn7e" + }, + { + "ratePercent": 0.1, + "validatorAddress": "secretvaloper1dpajv4su00mzf4r7rhzytc0vs23njkh2wll6lp" + }, + { + "ratePercent": 0.01, + "validatorAddress": "secretvaloper1dyfats3mqaphz7wyj8r89lt3gze88eenwu7tmf" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1dc6lhau0gnqh9rup2zv7z2jj4q9wwtkcm3edmq" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1wg085tj9xtv84q9l2zgdml46n6ju3mryun4fsu" + }, + { + "ratePercent": 0.04, + "validatorAddress": "secretvaloper1wt0s0zmvvf3rcdufeut8c9w9cr5n4mc7y0xu0f" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1w3lugsau69vm0kl9rxnf7wd8w68tr2ehlq00w0" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1sa8av4qw3xerr58kwvnm8wvd87zgp36mv6cnyg" + }, + { + "ratePercent": 0.2, + "validatorAddress": "secretvaloper13dryehdzcwj555fg9jwatyutvmhmskzna4azxq" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1jgx4pn3acae9esq5zha5ym3kzhq6x60frjwkrp" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1nqxdqq6qhtaj9m2t2mgvzds0xa6fa2g9sk5ymu" + }, + { + "ratePercent": 0.1, + "validatorAddress": "secretvaloper1nsf2zmhe3ktn5fqtrpffnktc4g7uag05ggse54" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1njvnz9dyd2suqzsuetxvfdsu6pjx5yxrpt3wr0" + }, + { + "ratePercent": 0.08, + "validatorAddress": "secretvaloper1nnt3t7ms82vf86jwq88zvwvzvm2mkhxx67zl3z" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1nc2justrp64paes6vrkk5atqxheqkjnl5sc76q" + }, + { + "ratePercent": 0.01, + "validatorAddress": "secretvaloper15q3f508smwa0v79qd0rjkdpfs475yd8vm0lr2n" + }, + { + "ratePercent": 0.08, + "validatorAddress": "secretvaloper15urq2dtp9qce4fyc85m6upwm9xul30490eylnc" + }, + { + "ratePercent": 0.13, + "validatorAddress": "secretvaloper1404h4aet3jn8uw7660670nqej2plsgqul74mjp" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper14cmhv376dth9tm3shcuextyhksu8cnfs0kq76p" + }, + { + "ratePercent": 0.02, + "validatorAddress": "secretvaloper14mwwdad00y7lwwmmk3yw2l2qhn6jzjpy28fmfw" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1kn73a34p004dzjyf0mzwmluh2692nptxkkaft9" + }, + { + "ratePercent": 0.1, + "validatorAddress": "secretvaloper1keq6g42q5vxq86cg7xrcng4tmxer8ufvcakg6u" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1hqlvlvps8rrd2jn05p3ugetu7z40z8vzszfnx9" + }, + { + "ratePercent": 0.01, + "validatorAddress": "secretvaloper1hzryhvapnq4x6cvx0yakd49vzkld5wx25h00fd" + }, + { + "ratePercent": 0.1, + "validatorAddress": "secretvaloper1hf2y3x956cw9v43ruj060zjdamwh9xs2h02gyg" + }, + { + "ratePercent": 0.04, + "validatorAddress": "secretvaloper1hscf4cjrhzsea5an5smt4z9aezhh4sf5jjrqka" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1hjd20hjvkx06y8p42xl0uzr3gr3ue3nkvd79jj" + }, + { + "ratePercent": 0.08, + "validatorAddress": "secretvaloper1cpp4s3t962tsn737fj4v8jr7net50qgxpn3nap" + }, + { + "ratePercent": 0.1, + "validatorAddress": "secretvaloper1c9g5k7svhxpl8vq6az25npwugj6m2srlnd3qcr" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1en4uz9wpztlcyexz8xgza4rkjc6l0a06lzsz09" + }, + { + "ratePercent": 0.04, + "validatorAddress": "secretvaloper16p9uqwcq2gvz75y5p9zvhn7vek9ra9zfunwyf0" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper16zkp5ngqkxtpr4wz3ew7jhyq2elsyaer6xj32s" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper16w5hlcf389le2n60t32eqf43plp539ged9sruy" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper16k26akna7h295rfjx3278s7xusnt736vy437y8" + }, + { + "ratePercent": 0.02, + "validatorAddress": "secretvaloper1mu525lvsdafu2f2s5ngf2qc5vufvmwej9ww64l" + }, + { + "ratePercent": 1, + "validatorAddress": "secretvaloper1utjz56dymhnu97jdzy8rhpwp9y97pt7duaa5ut" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1arfql9hv2m8nq3ssyjd0z8t4r4vh5senz5sj00" + }, + { + "ratePercent": 1, + "validatorAddress": "secretvaloper1agl5a3nlthtkxm7z58z4ttfcu348pt7wd06w9a" + }, + { + "ratePercent": 0.03, + "validatorAddress": "secretvaloper1ahawe276d250zpxt0xgpfg63ymmu63a0svuvgw" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper17m3c795fz4f36zjgjhr3vkf7e9pn3xxuryeww9" + }, + { + "ratePercent": 0, + "validatorAddress": "secretvaloper1lyafdwwzwfmjc8zhsew5pdcdu46na6pg09fql4" + }, + { + "ratePercent": 0.05, + "validatorAddress": "secretvaloper1larnhgur2ts7hlhphmtk65c3qz6dt52y79szst" + } + ] +} + diff --git a/src/test/mocks/shadeStaking/response.ts b/src/test/mocks/shadeStaking/response.ts new file mode 100644 index 0000000..4fc5710 --- /dev/null +++ b/src/test/mocks/shadeStaking/response.ts @@ -0,0 +1,295 @@ +export const stakingOpportunityResponseParsed = { + stakeTokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + totalStakedRaw: '158473829064218', + unbondingPeriod: 604800, + rewardPools: [ + { + id: '1', + amountRaw: '500000000000', + startDate: new Date('2023-06-27T19:00:00.000Z'), + endDate: new Date('2023-07-27T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '192901.234567901234567901', + }, + { + id: '2', + amountRaw: '500000000000', + startDate: new Date('2023-07-27T21:30:00.000Z'), + endDate: new Date('2023-08-27T21:30:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '186678.614097968936678614', + }, + { + id: '3', + amountRaw: '1000', + startDate: new Date('2023-08-10T11:51:20.000Z'), + endDate: new Date('2023-08-10T12:51:20.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '0.277777777777777777', + }, + { + id: '4', + amountRaw: '617000000', + startDate: new Date('2023-08-27T19:00:00.000Z'), + endDate: new Date('2023-09-03T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '1020.171957671957671957', + }, + { + id: '5', + amountRaw: '61083000000', + startDate: new Date('2023-08-27T22:40:00.000Z'), + endDate: new Date('2023-09-03T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '103250.507099391480730223', + }, + { + id: '6', + amountRaw: '83547000000', + startDate: new Date('2023-09-03T17:00:00.000Z'), + endDate: new Date('2023-09-10T17:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '138139.880952380952380952', + }, + { + id: '7', + amountRaw: '67263000000', + startDate: new Date('2023-09-10T17:00:00.000Z'), + endDate: new Date('2023-09-17T17:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '111215.277777777777777777', + }, + { + id: '8', + amountRaw: '67000000000', + startDate: new Date('2023-09-17T17:00:00.000Z'), + endDate: new Date('2023-09-23T17:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '129243.827160493827160493', + }, + { + id: '9', + amountRaw: '93983000000', + startDate: new Date('2023-09-24T16:00:00.000Z'), + endDate: new Date('2023-10-01T17:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '154475.673898750821827744', + }, + { + id: '10', + amountRaw: '74500000000', + startDate: new Date('2023-10-02T17:00:00.000Z'), + endDate: new Date('2023-10-09T17:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '123181.216931216931216931', + }, + { + id: '11', + amountRaw: '119713000000', + startDate: new Date('2023-10-10T02:57:00.000Z'), + endDate: new Date('2023-10-16T17:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '210399.3110478399943759', + }, + { + id: '12', + amountRaw: '86519000000', + startDate: new Date('2023-10-16T17:00:00.000Z'), + endDate: new Date('2023-10-23T17:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '143053.902116402116402116', + }, + { + id: '13', + amountRaw: '76224000000', + startDate: new Date('2023-10-23T19:45:00.000Z'), + endDate: new Date('2023-10-30T17:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '128129.097327281896116994', + }, + { + id: '14', + amountRaw: '130642000000', + startDate: new Date('2023-10-30T18:03:00.000Z'), + endDate: new Date('2023-11-06T18:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '216072.905295888326552214', + }, + { + id: '15', + amountRaw: '138150000000', + startDate: new Date('2023-11-06T18:00:00.000Z'), + endDate: new Date('2023-11-13T18:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '228422.619047619047619047', + }, + { + id: '16', + amountRaw: '152100000000', + startDate: new Date('2023-11-13T19:00:00.000Z'), + endDate: new Date('2023-11-20T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '251488.095238095238095238', + }, + { + id: '17', + amountRaw: '163200000000', + startDate: new Date('2023-11-20T19:25:00.000Z'), + endDate: new Date('2023-11-27T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '270512.182993535554450522', + }, + { + id: '18', + amountRaw: '130100000000', + startDate: new Date('2023-11-27T19:15:00.000Z'), + endDate: new Date('2023-12-04T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '215433.0187117072362974', + }, + { + id: '19', + amountRaw: '109100000000', + startDate: new Date('2023-12-04T19:50:00.000Z'), + endDate: new Date('2023-12-11T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '181289.464938517779993353', + }, + { + id: '20', + amountRaw: '114500000000', + startDate: new Date('2023-12-11T19:00:00.000Z'), + endDate: new Date('2023-12-18T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '189318.783068783068783068', + }, + { + id: '21', + amountRaw: '233600000000', + startDate: new Date('2023-12-18T19:35:00.000Z'), + endDate: new Date('2023-12-25T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '387589.182014269122283059', + }, + { + id: '22', + amountRaw: '240100000000', + startDate: new Date('2023-12-25T19:00:00.000Z'), + endDate: new Date('2024-01-01T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '396990.74074074074074074', + }, + { + id: '23', + amountRaw: '340600000000', + startDate: new Date('2024-01-01T19:30:00.000Z'), + endDate: new Date('2024-01-08T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '564842.454394693200663349', + }, + { + id: '24', + amountRaw: '223500000000', + startDate: new Date('2024-01-08T21:50:00.000Z'), + endDate: new Date('2024-01-15T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '375882.94651866801210898', + }, + { + id: '25', + amountRaw: '471200000000', + startDate: new Date('2024-01-15T20:06:00.000Z'), + endDate: new Date('2024-01-22T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '784235.403768058052060448', + }, + { + id: '26', + amountRaw: '216300000000', + startDate: new Date('2024-01-22T19:37:00.000Z'), + endDate: new Date('2024-01-29T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '358956.487105446579707258', + }, + { + id: '27', + amountRaw: '129258999999', + startDate: new Date('2024-01-29T20:30:00.000Z'), + endDate: new Date('2024-02-05T21:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '213087.701943620178041543', + }, + { + id: '28', + amountRaw: '211330000000', + startDate: new Date('2024-02-05T19:50:00.000Z'), + endDate: new Date('2024-02-12T20:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '349074.991740997687479352', + }, + { + id: '29', + amountRaw: '207864000000', + startDate: new Date('2024-02-12T20:40:00.000Z'), + endDate: new Date('2024-02-19T18:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '349233.87096774193548387', + }, + { + id: '30', + amountRaw: '626881000000', + startDate: new Date('2024-02-19T19:00:00.000Z'), + endDate: new Date('2024-02-26T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '1036509.589947089947089947', + }, + { + id: '31', + amountRaw: '11658282814', + startDate: new Date('2024-02-26T19:35:00.000Z'), + endDate: new Date('2024-02-27T01:26:31.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '552761.026663344131572389', + }, + { + id: '32', + amountRaw: '653810717186', + startDate: new Date('2024-02-27T01:40:00.000Z'), + endDate: new Date('2024-03-04T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '1125707.157689393939393939', + }, + { + id: '33', + amountRaw: '454524000000', + startDate: new Date('2024-03-04T19:00:00.000Z'), + endDate: new Date('2024-03-11T18:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '756027.944111776447105788', + }, + { + id: '34', + amountRaw: '207500000000', + startDate: new Date('2024-03-11T19:45:00.000Z'), + endDate: new Date('2024-03-18T18:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '346700.0835421888053467', + }, + { + id: '35', + amountRaw: '190059000000', + startDate: new Date('2024-03-18T20:00:00.000Z'), + endDate: new Date('2024-03-25T18:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '318037.14859437751004016', + }, + { + id: '36', + amountRaw: '155826000000', + startDate: new Date('2024-03-25T18:00:00.000Z'), + endDate: new Date('2024-04-01T18:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '257648.809523809523809523', + }, + ], +}; diff --git a/src/test/mocks/shadeStaking/stakingOpportunityResponse.json b/src/test/mocks/shadeStaking/stakingOpportunityResponse.json new file mode 100644 index 0000000..a665dcc --- /dev/null +++ b/src/test/mocks/shadeStaking/stakingOpportunityResponse.json @@ -0,0 +1,444 @@ +{ + "staking_info": { + "info": { + "stake_token": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "total_staked": "158473829064218", + "unbond_period": "604800", + "reward_pools": [ + { + "id": "1", + "amount": "500000000000", + "start": "1687892400", + "end": "1690484400", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "192901234567901234567901", + "official": true + }, + { + "id": "2", + "amount": "500000000000", + "start": "1690493400", + "end": "1693171800", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "186678614097968936678614", + "official": true + }, + { + "id": "3", + "amount": "1000", + "start": "1691668280", + "end": "1691671880", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "277777777777777777", + "official": true + }, + { + "id": "4", + "amount": "617000000", + "start": "1693162800", + "end": "1693767600", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "1020171957671957671957", + "official": true + }, + { + "id": "5", + "amount": "61083000000", + "start": "1693176000", + "end": "1693767600", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "103250507099391480730223", + "official": true + }, + { + "id": "6", + "amount": "83547000000", + "start": "1693760400", + "end": "1694365200", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "138139880952380952380952", + "official": true + }, + { + "id": "7", + "amount": "67263000000", + "start": "1694365200", + "end": "1694970000", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "111215277777777777777777", + "official": true + }, + { + "id": "8", + "amount": "67000000000", + "start": "1694970000", + "end": "1695488400", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "129243827160493827160493", + "official": true + }, + { + "id": "9", + "amount": "93983000000", + "start": "1695571200", + "end": "1696179600", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "154475673898750821827744", + "official": true + }, + { + "id": "10", + "amount": "74500000000", + "start": "1696266000", + "end": "1696870800", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "123181216931216931216931", + "official": true + }, + { + "id": "11", + "amount": "119713000000", + "start": "1696906620", + "end": "1697475600", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "210399311047839994375900", + "official": true + }, + { + "id": "12", + "amount": "86519000000", + "start": "1697475600", + "end": "1698080400", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "143053902116402116402116", + "official": true + }, + { + "id": "13", + "amount": "76224000000", + "start": "1698090300", + "end": "1698685200", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "128129097327281896116994", + "official": true + }, + { + "id": "14", + "amount": "130642000000", + "start": "1698688980", + "end": "1699293600", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "216072905295888326552214", + "official": true + }, + { + "id": "15", + "amount": "138150000000", + "start": "1699293600", + "end": "1699898400", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "228422619047619047619047", + "official": true + }, + { + "id": "16", + "amount": "152100000000", + "start": "1699902000", + "end": "1700506800", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "251488095238095238095238", + "official": true + }, + { + "id": "17", + "amount": "163200000000", + "start": "1700508300", + "end": "1701111600", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "270512182993535554450522", + "official": true + }, + { + "id": "18", + "amount": "130100000000", + "start": "1701112500", + "end": "1701716400", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "215433018711707236297400", + "official": true + }, + { + "id": "19", + "amount": "109100000000", + "start": "1701719400", + "end": "1702321200", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "181289464938517779993353", + "official": true + }, + { + "id": "20", + "amount": "114500000000", + "start": "1702321200", + "end": "1702926000", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "189318783068783068783068", + "official": true + }, + { + "id": "21", + "amount": "233600000000", + "start": "1702928100", + "end": "1703530800", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "387589182014269122283059", + "official": true + }, + { + "id": "22", + "amount": "240100000000", + "start": "1703530800", + "end": "1704135600", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "396990740740740740740740", + "official": true + }, + { + "id": "23", + "amount": "340600000000", + "start": "1704137400", + "end": "1704740400", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "564842454394693200663349", + "official": true + }, + { + "id": "24", + "amount": "223500000000", + "start": "1704750600", + "end": "1705345200", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "375882946518668012108980", + "official": true + }, + { + "id": "25", + "amount": "471200000000", + "start": "1705349160", + "end": "1705950000", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "784235403768058052060448", + "official": true + }, + { + "id": "26", + "amount": "216300000000", + "start": "1705952220", + "end": "1706554800", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "358956487105446579707258", + "official": true + }, + { + "id": "27", + "amount": "129258999999", + "start": "1706560200", + "end": "1707166800", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "213087701943620178041543", + "official": true + }, + { + "id": "28", + "amount": "211330000000", + "start": "1707162600", + "end": "1707768000", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "349074991740997687479352", + "official": true + }, + { + "id": "29", + "amount": "207864000000", + "start": "1707770400", + "end": "1708365600", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "349233870967741935483870", + "official": true + }, + { + "id": "30", + "amount": "626881000000", + "start": "1708369200", + "end": "1708974000", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "1036509589947089947089947", + "official": true + }, + { + "id": "31", + "amount": "11658282814", + "start": "1708976100", + "end": "1708997191", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "552761026663344131572389", + "official": true + }, + { + "id": "32", + "amount": "653810717186", + "start": "1708998000", + "end": "1709578800", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "1125707157689393939393939", + "official": true + }, + { + "id": "33", + "amount": "454524000000", + "start": "1709578800", + "end": "1710180000", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "756027944111776447105788", + "official": true + }, + { + "id": "34", + "amount": "207500000000", + "start": "1710186300", + "end": "1710784800", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "346700083542188805346700", + "official": true + }, + { + "id": "35", + "amount": "190059000000", + "start": "1710792000", + "end": "1711389600", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "318037148594377510040160", + "official": true + }, + { + "id": "36", + "amount": "155826000000", + "start": "1711389600", + "end": "1711994400", + "token": { + "address": "secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm", + "code_hash": "638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e" + }, + "rate": "257648809523809523809523", + "official": true + } + ] + } + } +} + diff --git a/src/types/apy.ts b/src/types/apy.ts new file mode 100644 index 0000000..b1db4ac --- /dev/null +++ b/src/types/apy.ts @@ -0,0 +1,66 @@ +type SecretInflationResponse = { + result: number, +}; + +type SecretSupplyItemResponse = { + denom: string, + amount: string, +} + +type SecretTotalSupplyResponse = { + amount: SecretSupplyItemResponse, +} + +type SecretTotalStakedResponse = { + result: { + not_bonded_tokens: string, + bonded_tokens: string, + }, +} + +type SecretTaxesResponse = { + result: { + community_tax: string, + secret_foundation_tax: string, + }, +} + +type SecretValidatorItemResponse = { + operator_address: string, + commission: { + commission_rates: { + rate: string, + }, + }, +} + +type SecretValidatorsResponse = { + result: SecretValidatorItemResponse[], +} + +type ValidatorRate = { + validatorAddress: string, + ratePercent: number, +} + +type SecretChainDataQueryModel = { + secretInflationPercent: number, + secretTotalSupplyRaw: number, + secretTotalStakedRaw: number, + secretTaxes: { + foundationTaxPercent: number, + communityTaxPercent: number, + }, + secretValidators: ValidatorRate[], +} + +export type { + SecretInflationResponse, + SecretTotalSupplyResponse, + SecretTotalStakedResponse, + SecretTaxesResponse, + SecretValidatorItemResponse, + SecretValidatorsResponse, + ValidatorRate, + SecretChainDataQueryModel, +}; diff --git a/src/types/contracts/index.ts b/src/types/contracts/index.ts index 5af7c35..e57606d 100644 --- a/src/types/contracts/index.ts +++ b/src/types/contracts/index.ts @@ -5,3 +5,4 @@ export * from './snip20'; export * from './swap'; export * from './derivativeScrt'; export * from './derivativeShd'; +export * from './shadeStaking'; diff --git a/src/types/contracts/shadeStaking/index.ts b/src/types/contracts/shadeStaking/index.ts new file mode 100644 index 0000000..56ca536 --- /dev/null +++ b/src/types/contracts/shadeStaking/index.ts @@ -0,0 +1,43 @@ +type StakingInfoServiceResponse = { + staking_info: { + info: { + stake_token: string, + total_staked: string, + unbond_period: string, + reward_pools: [{ + id: string, + amount: string, + start: string, + end: string, + token: { + address: string, + code_hash: string, + }, + rate: string, + official: boolean, + }], + }, + }, +} + +type StakingRewardPoolServiceModel = { + id: string, + amountRaw: string, + startDate: Date, + endDate: Date, + tokenAddress: string, + rateRaw: string, +} + +type StakingInfoServiceModel = { + stakeTokenAddress: string, + totalStakedRaw: string, + unbondingPeriod: number, + rewardPools: StakingRewardPoolServiceModel[], +} + +export type { + StakingInfoServiceResponse, + StakingRewardPoolServiceModel, + StakingInfoServiceModel, +};