diff --git a/.changeset/swift-mirrors-cross.md b/.changeset/swift-mirrors-cross.md new file mode 100644 index 0000000..5440b8f --- /dev/null +++ b/.changeset/swift-mirrors-cross.md @@ -0,0 +1,5 @@ +--- +"@shadeprotocol/shadejs": patch +--- + +add batch query for shade staking diff --git a/src/contracts/services/shadeStaking.test.ts b/src/contracts/services/shadeStaking.test.ts index a949f56..2e6c3d6 100644 --- a/src/contracts/services/shadeStaking.test.ts +++ b/src/contracts/services/shadeStaking.test.ts @@ -8,13 +8,18 @@ import { of } from 'rxjs'; import stakingOpportunityResponse from '~/test/mocks/shadeStaking/stakingOpportunityResponse.json'; import { stakingOpportunityResponseParsed } from '~/test/mocks/shadeStaking/response'; import { + batchQueryShadeStakingOpportunity, + batchQueryShadeStakingOpportunity$, parseStakingOpportunity, queryShadeStakingOpportunity, queryShadeStakingOpportunity$, } from '~/contracts/services/shadeStaking'; import { StakingInfoServiceResponse } from '~/types/contracts/shadeStaking/index'; +import { batchStakingInfoUnparsed } from '~/test/mocks/shadeStaking/batchStakingInfoUnparsed'; +import { batchStakingInfoParsed } from '~/test/mocks/shadeStaking/batchStakingInfoParsed'; const sendSecretClientContractQuery$ = vi.hoisted(() => vi.fn()); +const batchQuery$ = vi.hoisted(() => vi.fn()); beforeAll(() => { vi.mock('~/contracts/definitions/shadeStaking', () => ({ @@ -28,6 +33,10 @@ beforeAll(() => { vi.mock('~/client/services/clientServices', () => ({ sendSecretClientContractQuery$, })); + + vi.mock('~/contracts/services/batchQuery', () => ({ + batchQuery$, + })); }); test('it can parse the shade staking info', () => { @@ -76,3 +85,60 @@ test('it can call the query shade staking info service', async () => { expect(output2).toStrictEqual(stakingOpportunityResponseParsed); }); + +test('it can call the batch shade staking info query service', async () => { + const input = { + queryRouterContractAddress: 'CONTRACT_ADDRESS', + queryRouterCodeHash: 'CODE_HASH', + lcdEndpoint: 'LCD_ENDPOINT', + chainId: 'CHAIN_ID', + stakingContracts: [{ + address: 'STAKING_ADDRESS', + codeHash: 'STAKING_CODE_HASH', + }], + }; + // observables function + batchQuery$.mockReturnValueOnce(of(batchStakingInfoUnparsed)); + let output; + batchQueryShadeStakingOpportunity$(input).subscribe({ + next: (response) => { + output = response; + }, + }); + + expect(batchQuery$).toHaveBeenCalledWith({ + contractAddress: input.queryRouterContractAddress, + codeHash: input.queryRouterCodeHash, + lcdEndpoint: input.lcdEndpoint, + chainId: input.chainId, + queries: [{ + id: input.stakingContracts[0].address, + contract: { + address: input.stakingContracts[0].address, + codeHash: input.stakingContracts[0].codeHash, + }, + queryMsg: 'STAKING_INFO_MSG', + }], + }); + + expect(output).toStrictEqual(batchStakingInfoParsed); + + // async/await function + batchQuery$.mockReturnValueOnce(of(batchStakingInfoUnparsed)); + const response = await batchQueryShadeStakingOpportunity(input); + expect(batchQuery$).toHaveBeenCalledWith({ + contractAddress: input.queryRouterContractAddress, + codeHash: input.queryRouterCodeHash, + lcdEndpoint: input.lcdEndpoint, + chainId: input.chainId, + queries: [{ + id: input.stakingContracts[0].address, + contract: { + address: input.stakingContracts[0].address, + codeHash: input.stakingContracts[0].codeHash, + }, + queryMsg: 'STAKING_INFO_MSG', + }], + }); + expect(response).toStrictEqual(batchStakingInfoParsed); +}); diff --git a/src/contracts/services/shadeStaking.ts b/src/contracts/services/shadeStaking.ts index 27695bf..b6cccb8 100644 --- a/src/contracts/services/shadeStaking.ts +++ b/src/contracts/services/shadeStaking.ts @@ -12,7 +12,15 @@ import { StakingInfoServiceResponse, StakingRewardPoolServiceModel, StakingInfoServiceModel, + BatchShadeStakingOpportunity, } from '~/types/contracts/shadeStaking/index'; +import { + BatchQueryParams, + BatchQueryParsedResponse, + Contract, + MinBlockHeightValidationOptions, +} from '~/types'; +import { batchQuery$ } from './batchQuery'; // data returned from the contract in normalized form with // 18 decimals, in addition to any decimals on the individual token @@ -42,6 +50,18 @@ function parseStakingOpportunity(data: StakingInfoServiceResponse): StakingInfoS }; } +/** + * parses the staking info reponse from a batch query of + * multiple staking contracts + */ +const parseBatchQueryShadeStakingOpportunityResponse = ( + response: BatchQueryParsedResponse, +): BatchShadeStakingOpportunity => response.map((item) => ({ + stakingContractAddress: item.id as string, + stakingInfo: parseStakingOpportunity(item.response), + blockHeight: item.blockHeight, +})); + /** * query the staking info from the shade staking contract */ @@ -88,8 +108,77 @@ async function queryShadeStakingOpportunity({ })); } +/** + * query the staking info for multiple staking contracts at one time + */ +function batchQueryShadeStakingOpportunity$({ + queryRouterContractAddress, + queryRouterCodeHash, + lcdEndpoint, + chainId, + stakingContracts, + minBlockHeightValidationOptions, +}:{ + queryRouterContractAddress: string, + queryRouterCodeHash?: string, + lcdEndpoint?: string, + chainId?: string, + stakingContracts: Contract[] + minBlockHeightValidationOptions?: MinBlockHeightValidationOptions, +}) { + const queries:BatchQueryParams[] = stakingContracts.map((contract) => ({ + id: contract.address, + contract: { + address: contract.address, + codeHash: contract.codeHash, + }, + queryMsg: msgQueryShadeStakingOpportunity(), + })); + return batchQuery$({ + contractAddress: queryRouterContractAddress, + codeHash: queryRouterCodeHash, + lcdEndpoint, + chainId, + queries, + minBlockHeightValidationOptions, + }).pipe( + map(parseBatchQueryShadeStakingOpportunityResponse), + first(), + ); +} + +/** + * query the staking info for multiple staking contracts at one time + */ +async function batchQueryShadeStakingOpportunity({ + queryRouterContractAddress, + queryRouterCodeHash, + lcdEndpoint, + chainId, + stakingContracts, + minBlockHeightValidationOptions, +}:{ + queryRouterContractAddress: string, + queryRouterCodeHash?: string, + lcdEndpoint?: string, + chainId?: string, + stakingContracts: Contract[] + minBlockHeightValidationOptions?: MinBlockHeightValidationOptions, +}) { + return lastValueFrom(batchQueryShadeStakingOpportunity$({ + queryRouterContractAddress, + queryRouterCodeHash, + lcdEndpoint, + chainId, + stakingContracts, + minBlockHeightValidationOptions, + })); +} + export { parseStakingOpportunity, queryShadeStakingOpportunity$, queryShadeStakingOpportunity, + batchQueryShadeStakingOpportunity$, + batchQueryShadeStakingOpportunity, }; diff --git a/src/test/mocks/shadeStaking/batchStakingInfoParsed.ts b/src/test/mocks/shadeStaking/batchStakingInfoParsed.ts new file mode 100644 index 0000000..48a501b --- /dev/null +++ b/src/test/mocks/shadeStaking/batchStakingInfoParsed.ts @@ -0,0 +1,42 @@ +const batchStakingInfoParsed = [ + { + stakingContractAddress: 'secret17ue98qd2akjazu2w2r95cz06mh8pfl3v5hva4j', + stakingInfo: { + 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', + }, + ], + }, + blockHeight: 1, + }, + { + stakingContractAddress: 'secret17ue98qd2akjazu2w2r95cz06mh8pfl3v5hva4j', + stakingInfo: { + stakeTokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + totalStakedRaw: '258473829064218', + unbondingPeriod: 704800, + rewardPools: [ + { + id: '1', + amountRaw: '600000000000', + startDate: new Date('2023-06-27T19:00:00.000Z'), + endDate: new Date('2023-07-27T19:00:00.000Z'), + tokenAddress: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + rateRaw: '292901.234567901234567901', + }, + ], + }, + blockHeight: 1, + }, +]; + +export { batchStakingInfoParsed }; diff --git a/src/test/mocks/shadeStaking/batchStakingInfoUnparsed.ts b/src/test/mocks/shadeStaking/batchStakingInfoUnparsed.ts new file mode 100644 index 0000000..f458a1e --- /dev/null +++ b/src/test/mocks/shadeStaking/batchStakingInfoUnparsed.ts @@ -0,0 +1,60 @@ +const batchStakingInfoUnparsed = [ + { + id: 'secret17ue98qd2akjazu2w2r95cz06mh8pfl3v5hva4j', + response: { + 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, + }, + ], + }, + }, + }, + blockHeight: 1, + }, + { + id: 'secret17ue98qd2akjazu2w2r95cz06mh8pfl3v5hva4j', + response: { + staking_info: { + info: { + stake_token: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + total_staked: '258473829064218', + unbond_period: '704800', + reward_pools: [ + { + id: '1', + amount: '600000000000', + start: '1687892400', + end: '1690484400', + token: { + address: 'secret153wu605vvp934xhd4k9dtd640zsep5jkesstdm', + code_hash: '638a3e1d50175fbcb8373cf801565283e3eb23d88a9b7b7f99fcc5eb1e6b561e', + }, + rate: '292901234567901234567901', + official: true, + }, + ], + }, + }, + }, + blockHeight: 1, + }, +]; + +export { + batchStakingInfoUnparsed, +}; diff --git a/src/types/contracts/shadeStaking/index.ts b/src/types/contracts/shadeStaking/index.ts index 56ca536..c2815d8 100644 --- a/src/types/contracts/shadeStaking/index.ts +++ b/src/types/contracts/shadeStaking/index.ts @@ -36,8 +36,17 @@ type StakingInfoServiceModel = { rewardPools: StakingRewardPoolServiceModel[], } +type BatchSingleShadeStakingOpportunity = { + stakingContractAddress: string, + stakingInfo: StakingInfoServiceModel, + blockHeight: number, + } + + type BatchShadeStakingOpportunity = BatchSingleShadeStakingOpportunity[] + export type { StakingInfoServiceResponse, StakingRewardPoolServiceModel, StakingInfoServiceModel, + BatchShadeStakingOpportunity, };