diff --git a/src/contracts/services/oracle.test.ts b/src/contracts/services/oracle.test.ts index 563615b..0e55504 100644 --- a/src/contracts/services/oracle.test.ts +++ b/src/contracts/services/oracle.test.ts @@ -7,26 +7,29 @@ import { } from 'vitest'; import { parsePriceFromContract, - parsePricesFromContract, - parseBatchQueryIndividualPrices, + parseBatchPrices, queryPrice$, queryPrices$, queryPrice, queryPrices, batchQueryIndividualPrices, batchQueryIndividualPrices$, + parseBatchPrice, + parseBatchQueryIndividualPrices, } from '~/contracts/services/oracle'; import priceResponse from '~/test/mocks/oracle/priceResponse.json'; -import pricesResponse from '~/test/mocks/oracle/pricesResponse.json'; import { batchPricesWithErrorParsed } from '~/test/mocks/batchQuery/batchPricesWithErrorParsed'; import { of } from 'rxjs'; import { priceParsed, pricesParsed, } from '~/test/mocks/oracle/pricesParsed'; -import { batchPricesWithErrorParsedResponse } from '~/test/mocks/oracle/batchPricesParsed'; +import { + batchPricesWithErrorParsedResponse, + batchPricesParsedResponse, +} from '~/test/mocks/oracle/batchPrices'; +import { batchPrice } from '~/test/mocks/oracle/batchPrice'; -const sendSecretClientContractQuery$ = vi.hoisted(() => vi.fn()); const batchQuery$ = vi.hoisted(() => vi.fn()); beforeAll(() => { @@ -39,10 +42,6 @@ beforeAll(() => { getActiveQueryClient$: vi.fn(() => of({ client: 'CLIENT' })), })); - vi.mock('~/client/services/clientServices', () => ({ - sendSecretClientContractQuery$, - })); - vi.mock('~/contracts/services/batchQuery', () => ({ batchQuery$, })); @@ -52,35 +51,46 @@ afterAll(() => { vi.clearAllMocks(); }); -test('it can parse the price response', () => { +test('it can parse the price contract response', () => { expect(parsePriceFromContract( priceResponse, + 123456789, )).toStrictEqual(priceParsed); }); -test('it can parse the prices response', () => { - expect(parsePricesFromContract( - pricesResponse, - )).toStrictEqual(pricesParsed); +test('it can parse the batch single price response', () => { + expect(parseBatchPrice( + batchPrice, + )).toStrictEqual(priceParsed); }); test('it can parse the batch individual prices response', () => { expect(parseBatchQueryIndividualPrices( batchPricesWithErrorParsed, )).toStrictEqual(batchPricesWithErrorParsedResponse); + + expect(parseBatchPrices( + batchPricesParsedResponse, + )).toStrictEqual(pricesParsed); }); test('it can send the query single price service', async () => { const input = { - contractAddress: 'CONTRACT_ADDRESS', - codeHash: 'CODE_HASH', + queryRouterContractAddress: 'QUERY_ROUTER_CONTRACT_ADDRESS', + queryRouterCodeHash: 'QUERY_ROUTER_CODE_HASH', + oracleContractAddress: 'ORACLE_CONTRACT_ADDRESS', + oracleCodeHash: 'ORACLE_CODE_HASH', oracleKey: 'ORACLE_KEY', lcdEndpoint: 'LCD_ENDPOINT', chainId: 'CHAIN_ID', + minBlockHeightValidationOptions: { + minBlockHeight: 1, + maxRetries: 2, + }, }; // observables function - sendSecretClientContractQuery$.mockReturnValueOnce(of(priceResponse)); + batchQuery$.mockReturnValueOnce(of(batchPrice)); let output; queryPrice$(input).subscribe({ @@ -89,40 +99,49 @@ test('it can send the query single price service', async () => { }, }); - expect(sendSecretClientContractQuery$).toHaveBeenCalledWith({ - queryMsg: 'MSG_QUERY_ORACLE_PRICE', - client: 'CLIENT', - contractAddress: input.contractAddress, - codeHash: input.codeHash, - }); - + const batchQueryParams = { + contractAddress: input.queryRouterContractAddress, + codeHash: input.queryRouterCodeHash, + lcdEndpoint: input.lcdEndpoint, + chainId: input.chainId, + queries: [{ + id: 1, + contract: { + address: input.oracleContractAddress, + codeHash: input.oracleCodeHash, + }, + queryMsg: 'MSG_QUERY_ORACLE_PRICE', + }], + minBlockHeightValidationOptions: input.minBlockHeightValidationOptions, + }; + expect(batchQuery$).toHaveBeenCalledWith(batchQueryParams); expect(output).toStrictEqual(priceParsed); // async/await function - sendSecretClientContractQuery$.mockReturnValueOnce(of(priceResponse)); + batchQuery$.mockReturnValueOnce(of(batchPrice)); const response = await queryPrice(input); - expect(sendSecretClientContractQuery$).toHaveBeenCalledWith({ - queryMsg: 'MSG_QUERY_ORACLE_PRICE', - client: 'CLIENT', - contractAddress: input.contractAddress, - codeHash: input.codeHash, - }); - + expect(batchQuery$).toHaveBeenCalledWith(batchQueryParams); expect(response).toStrictEqual(priceParsed); }); test('it can send the query multiple prices service', async () => { const input = { - contractAddress: 'CONTRACT_ADDRESS', - codeHash: 'CODE_HASH', - oracleKeys: ['ORACLE_KEY'], + queryRouterContractAddress: 'QUERY_ROUTER_CONTRACT_ADDRESS', + queryRouterCodeHash: 'QUERY_ROUTER_CODE_HASH', + oracleContractAddress: 'ORACLE_CONTRACT_ADDRESS', + oracleCodeHash: 'ORACLE_CODE_HASH', + oracleKeys: ['ORACLE_KEY_1, ORACLE_KEY_2'], lcdEndpoint: 'LCD_ENDPOINT', chainId: 'CHAIN_ID', + minBlockHeightValidationOptions: { + minBlockHeight: 1, + maxRetries: 2, + }, }; // observables function - sendSecretClientContractQuery$.mockReturnValueOnce(of(pricesResponse)); + batchQuery$.mockReturnValueOnce(of(batchPricesParsedResponse)); let output; queryPrices$(input).subscribe({ @@ -131,26 +150,30 @@ test('it can send the query multiple prices service', async () => { }, }); - expect(sendSecretClientContractQuery$).toHaveBeenCalledWith({ - queryMsg: 'MSG_QUERY_ORACLE_PRICES', - client: 'CLIENT', - contractAddress: input.contractAddress, - codeHash: input.codeHash, - }); + const batchQueryParams = { + contractAddress: input.queryRouterContractAddress, + codeHash: input.queryRouterCodeHash, + lcdEndpoint: input.lcdEndpoint, + chainId: input.chainId, + queries: [{ + id: 1, + contract: { + address: input.oracleContractAddress, + codeHash: input.oracleCodeHash, + }, + queryMsg: 'MSG_QUERY_ORACLE_PRICES', + }], + minBlockHeightValidationOptions: input.minBlockHeightValidationOptions, + }; + expect(batchQuery$).toHaveBeenCalledWith(batchQueryParams); expect(output).toStrictEqual(pricesParsed); // async/await function - sendSecretClientContractQuery$.mockReturnValueOnce(of(pricesResponse)); + batchQuery$.mockReturnValueOnce(of(batchPricesParsedResponse)); const response = await queryPrices(input); - expect(sendSecretClientContractQuery$).toHaveBeenCalledWith({ - queryMsg: 'MSG_QUERY_ORACLE_PRICES', - client: 'CLIENT', - contractAddress: input.contractAddress, - codeHash: input.codeHash, - }); - + expect(batchQuery$).toHaveBeenCalledWith(batchQueryParams); expect(response).toStrictEqual(pricesParsed); }); diff --git a/src/contracts/services/oracle.ts b/src/contracts/services/oracle.ts index 9f4d37e..9555659 100644 --- a/src/contracts/services/oracle.ts +++ b/src/contracts/services/oracle.ts @@ -1,20 +1,14 @@ -import { - OraclePriceResponse, - OraclePricesResponse, -} from '~/types/contracts/oracle/response'; +import { OraclePriceResponse } from '~/types/contracts/oracle/response'; import { ParsedOraclePriceResponse, ParsedOraclePricesResponse, OracleErrorType, } from '~/types/contracts/oracle/model'; import { - switchMap, first, map, lastValueFrom, } from 'rxjs'; -import { sendSecretClientContractQuery$ } from '~/client/services/clientServices'; -import { getActiveQueryClient$ } from '~/client'; import { msgQueryOraclePrice, msgQueryOraclePrices } from '~/contracts/definitions/oracle'; import { BatchItemResponseStatus, @@ -29,7 +23,7 @@ import { batchQuery$ } from './batchQuery'; */ const parsePriceFromContract = ( response: OraclePriceResponse, - blockHeight?: number, + blockHeight: number, ): ParsedOraclePriceResponse => ({ oracleKey: response.key, rate: response.data.rate, @@ -39,19 +33,18 @@ const parsePriceFromContract = ( }); /** -* Parses the contract prices query into the app data model -*/ -function parsePricesFromContract(pricesResponse: OraclePricesResponse) { - return pricesResponse.reduce((prev, curr) => ({ - ...prev, - [curr.key]: { - oracleKey: curr.key, - rate: curr.data.rate, - lastUpdatedBase: curr.data.last_updated_base, - lastUpdatedQuote: curr.data.last_updated_quote, - } as ParsedOraclePriceResponse, - }), {} as ParsedOraclePricesResponse); -} + * parses the reponse from a batch query of + * multiple individual prices + */ +const parseBatchPrice = ( + response: BatchQueryParsedResponse, +): ParsedOraclePriceResponse => { + if (response.length > 1) { + throw new Error('Error parsing price, multiple prices returned when only 1 was expected'); + } + const { response: priceResponse, blockHeight } = response[0]; + return parsePriceFromContract(priceResponse as OraclePriceResponse, blockHeight); +}; /** * parses the reponse from a batch query of @@ -89,54 +82,112 @@ const parseBatchQueryIndividualPrices = ( }; }, {}); +/** + * parses the reponse from a batch query of + * multiple prices returned as a group + */ +const parseBatchPrices = ( + response: BatchQueryParsedResponse, +): ParsedOraclePricesResponse => { + if (response.length > 1) { + throw new Error('Error parsing prices, multiple groups of prices were returned when only 1 was expected'); + } + const pricesResponse = response[0]; + + if ( + pricesResponse.status + && pricesResponse.status === BatchItemResponseStatus.ERROR + ) { + let errorType = OracleErrorType.UNKNOWN; + if (pricesResponse.response.includes('Derivative rate is stale')) { + errorType = OracleErrorType.STALE_DERIVATIVE_RATE; + } + throw new Error(`ORACLE ERROR: ${errorType}`); + } + + return pricesResponse.response.reduce(( + acc:ParsedOraclePricesResponse, + curr: OraclePriceResponse, + ) => ({ + ...acc, + [curr.key]: parsePriceFromContract(curr, pricesResponse.blockHeight), + }), {} as ParsedOraclePricesResponse); +}; /** * query the price of an asset using the oracle key */ const queryPrice$ = ({ - contractAddress, - codeHash, + queryRouterContractAddress, + queryRouterCodeHash, + oracleContractAddress, + oracleCodeHash, oracleKey, lcdEndpoint, chainId, + minBlockHeightValidationOptions, }:{ - contractAddress: string, - codeHash?: string, + queryRouterContractAddress: string, + queryRouterCodeHash?: string, + oracleContractAddress: string, + oracleCodeHash: string, oracleKey: string, lcdEndpoint?: string, chainId?: string, -}) => getActiveQueryClient$(lcdEndpoint, chainId).pipe( - switchMap(({ client }) => sendSecretClientContractQuery$({ + minBlockHeightValidationOptions?: MinBlockHeightValidationOptions, +}) => { + const query: BatchQueryParams[] = [{ + id: 1, + contract: { + address: oracleContractAddress, + codeHash: oracleCodeHash, + }, queryMsg: msgQueryOraclePrice(oracleKey), - client, - contractAddress, - codeHash, - })), - map((response) => parsePriceFromContract(response as OraclePriceResponse)), - first(), -); + }]; + + return batchQuery$({ + contractAddress: queryRouterContractAddress, + codeHash: queryRouterCodeHash, + lcdEndpoint, + chainId, + queries: query, + minBlockHeightValidationOptions, + }).pipe( + map(parseBatchPrice), + first(), + ); +}; /** * query the price of an asset using the oracle key */ async function queryPrice({ - contractAddress, - codeHash, + queryRouterContractAddress, + queryRouterCodeHash, + oracleContractAddress, + oracleCodeHash, oracleKey, lcdEndpoint, chainId, + minBlockHeightValidationOptions, }:{ - contractAddress: string, - codeHash?: string, + queryRouterContractAddress: string, + queryRouterCodeHash?: string, + oracleContractAddress: string, + oracleCodeHash: string, oracleKey: string, lcdEndpoint?: string, chainId?: string, + minBlockHeightValidationOptions?: MinBlockHeightValidationOptions, }) { return lastValueFrom(queryPrice$({ - contractAddress, - codeHash, + queryRouterContractAddress, + queryRouterCodeHash, + oracleContractAddress, + oracleCodeHash, oracleKey, lcdEndpoint, chainId, + minBlockHeightValidationOptions, })); } @@ -144,50 +195,76 @@ async function queryPrice({ * query multiple asset prices using oracle keys */ const queryPrices$ = ({ - contractAddress, - codeHash, + queryRouterContractAddress, + queryRouterCodeHash, + oracleContractAddress, + oracleCodeHash, oracleKeys, lcdEndpoint, chainId, + minBlockHeightValidationOptions, }:{ - contractAddress: string, - codeHash?: string, + queryRouterContractAddress: string, + queryRouterCodeHash?: string, + oracleContractAddress: string, + oracleCodeHash: string, oracleKeys: string[], lcdEndpoint?: string, chainId?: string, -}) => getActiveQueryClient$(lcdEndpoint, chainId).pipe( - switchMap(({ client }) => sendSecretClientContractQuery$({ + minBlockHeightValidationOptions?: MinBlockHeightValidationOptions, +}) => { + const query: BatchQueryParams[] = [{ + id: 1, + contract: { + address: oracleContractAddress, + codeHash: oracleCodeHash, + }, queryMsg: msgQueryOraclePrices(oracleKeys), - client, - contractAddress, - codeHash, - })), - map((response) => parsePricesFromContract(response as OraclePricesResponse)), - first(), -); + }]; + return batchQuery$({ + contractAddress: queryRouterContractAddress, + codeHash: queryRouterCodeHash, + lcdEndpoint, + chainId, + queries: query, + minBlockHeightValidationOptions, + }).pipe( + map(parseBatchPrices), + first(), + ); +}; /** * query multiple asset prices using oracle keys */ async function queryPrices({ - contractAddress, - codeHash, + queryRouterContractAddress, + queryRouterCodeHash, + oracleContractAddress, + oracleCodeHash, oracleKeys, lcdEndpoint, chainId, + minBlockHeightValidationOptions, }:{ - contractAddress: string, - codeHash?: string, + queryRouterContractAddress: string, + queryRouterCodeHash?: string, + oracleContractAddress: string, + oracleCodeHash: string, oracleKeys: string[], lcdEndpoint?: string, chainId?: string, + minBlockHeightValidationOptions?: MinBlockHeightValidationOptions, }) { return lastValueFrom(queryPrices$({ - contractAddress, - codeHash, + queryRouterContractAddress, + queryRouterCodeHash, + oracleContractAddress, + oracleCodeHash, oracleKeys, lcdEndpoint, chainId, + minBlockHeightValidationOptions, })); } @@ -279,12 +356,13 @@ async function batchQueryIndividualPrices({ export { parsePriceFromContract, - parsePricesFromContract, queryPrice$, queryPrices$, queryPrice, queryPrices, - parseBatchQueryIndividualPrices, batchQueryIndividualPrices$, batchQueryIndividualPrices, + parseBatchPrice, + parseBatchPrices, + parseBatchQueryIndividualPrices, }; diff --git a/src/test/mocks/oracle/batchPrice.ts b/src/test/mocks/oracle/batchPrice.ts new file mode 100644 index 0000000..82dd98f --- /dev/null +++ b/src/test/mocks/oracle/batchPrice.ts @@ -0,0 +1,19 @@ +import { BatchItemResponseStatus, BatchQueryParsedResponse } from '~/types/contracts/batchQuery/model'; + +const batchPrice: BatchQueryParsedResponse = [{ + id: 1, + response: { + key: 'BTC', + data: { + rate: '27917207155600000000000', + last_updated_base: 1696644063, + last_updated_quote: 18446744073709552000, + }, + }, + status: BatchItemResponseStatus.SUCCESS, + blockHeight: 123456789, +}]; + +export { + batchPrice, +}; diff --git a/src/test/mocks/oracle/batchPricesParsed.ts b/src/test/mocks/oracle/batchPrices.ts similarity index 52% rename from src/test/mocks/oracle/batchPricesParsed.ts rename to src/test/mocks/oracle/batchPrices.ts index 4f5a33b..f372706 100644 --- a/src/test/mocks/oracle/batchPricesParsed.ts +++ b/src/test/mocks/oracle/batchPrices.ts @@ -1,4 +1,33 @@ -import { OracleErrorType, ParsedOraclePricesResponse } from '~/types'; +import { + BatchQueryParsedResponse, + OracleErrorType, + ParsedOraclePricesResponse, + BatchItemResponseStatus, +} from '~/types'; + +const batchPricesParsedResponse: BatchQueryParsedResponse = [{ + id: 1, + response: [ + { + key: 'BTC', + data: { + rate: '27917207155600000000000', + last_updated_base: 1696644063, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'ETH', + data: { + rate: '1644083682900000000000', + last_updated_base: 1696644063, + last_updated_quote: 18446744073709552000, + }, + }, + ], + status: BatchItemResponseStatus.SUCCESS, + blockHeight: 3, +}]; const batchPricesWithErrorParsedResponse: ParsedOraclePricesResponse = { BTC: { @@ -20,5 +49,6 @@ const batchPricesWithErrorParsedResponse: ParsedOraclePricesResponse = { }; export { + batchPricesParsedResponse, batchPricesWithErrorParsedResponse, }; diff --git a/src/test/mocks/oracle/pricesParsed.ts b/src/test/mocks/oracle/pricesParsed.ts index 9321ea2..201e197 100644 --- a/src/test/mocks/oracle/pricesParsed.ts +++ b/src/test/mocks/oracle/pricesParsed.ts @@ -3,7 +3,7 @@ const priceParsed = { rate: '27917207155600000000000', lastUpdatedBase: 1696644063, lastUpdatedQuote: 18446744073709552000, - blockHeight: undefined, + blockHeight: 123456789, }; const pricesParsed = { @@ -12,12 +12,14 @@ const pricesParsed = { rate: '27917207155600000000000', lastUpdatedBase: 1696644063, lastUpdatedQuote: 18446744073709552000, + blockHeight: 3, }, ETH: { oracleKey: 'ETH', rate: '1644083682900000000000', lastUpdatedBase: 1696644063, lastUpdatedQuote: 18446744073709552000, + blockHeight: 3, }, }; diff --git a/src/test/mocks/oracle/pricesResponse.json b/src/test/mocks/oracle/pricesResponse.json deleted file mode 100644 index 26e5289..0000000 --- a/src/test/mocks/oracle/pricesResponse.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "key": "BTC", - "data": { - "rate": "27917207155600000000000", - "last_updated_base": 1696644063, - "last_updated_quote": 18446744073709552000 - } - }, - { - "key": "ETH", - "data": { - "rate": "1644083682900000000000", - "last_updated_base": 1696644063, - "last_updated_quote": 18446744073709552000 - } - } -] \ No newline at end of file diff --git a/src/types/contracts/oracle/model.ts b/src/types/contracts/oracle/model.ts index 4e37daf..0a4f896 100644 --- a/src/types/contracts/oracle/model.ts +++ b/src/types/contracts/oracle/model.ts @@ -12,7 +12,7 @@ type ParsedOraclePriceResponse = { type: OracleErrorType, msg: any, }, - blockHeight?: number // block height is only available when using a batch query router + blockHeight: number } type ParsedOraclePricesResponse = {