From c49dea7ffd2eb03721401d6c15cb0b397675a4ed Mon Sep 17 00:00:00 2001 From: Austin Date: Fri, 26 Apr 2024 13:40:15 -0500 Subject: [PATCH 1/3] feat: silk basket query --- .changeset/spicy-boats-join.md | 5 + src/contracts/definitions/silkBasket.test.ts | 12 + src/contracts/definitions/silkBasket.ts | 10 + src/contracts/services/silkBasket.test.ts | 257 ++++++++++++++ src/contracts/services/silkBasket.ts | 328 ++++++++++++++++++ .../batchSilkBasketOraclePricesResponse.ts | 126 +++++++ .../batchSilkBasketWithBasketErrorResponse.ts | 70 ++++ ...lkBasketWithMissingOraclePricesResponse.ts | 118 +++++++ .../batchSilkBasketWithOracleErrorResponse.ts | 78 +++++ src/test/mocks/silkBasket/pricesResponse.json | 50 +++ src/test/mocks/silkBasket/silkBasketParsed.ts | 147 ++++++++ .../mocks/silkBasket/silkBasketResponse.json | 59 ++++ .../silkBasketWithMissingPricesParsed.ts | 76 ++++ .../silkBasketWithOracleErrorParsed.ts | 76 ++++ src/types/contracts/silkBasket/model.ts | 39 +++ src/types/contracts/silkBasket/response.ts | 27 ++ src/types/contracts/silkBasket/service.ts | 8 + 17 files changed, 1486 insertions(+) create mode 100644 .changeset/spicy-boats-join.md create mode 100644 src/contracts/definitions/silkBasket.test.ts create mode 100644 src/contracts/definitions/silkBasket.ts create mode 100644 src/contracts/services/silkBasket.test.ts create mode 100644 src/contracts/services/silkBasket.ts create mode 100644 src/test/mocks/silkBasket/batchSilkBasketOraclePricesResponse.ts create mode 100644 src/test/mocks/silkBasket/batchSilkBasketWithBasketErrorResponse.ts create mode 100644 src/test/mocks/silkBasket/batchSilkBasketWithMissingOraclePricesResponse.ts create mode 100644 src/test/mocks/silkBasket/batchSilkBasketWithOracleErrorResponse.ts create mode 100644 src/test/mocks/silkBasket/pricesResponse.json create mode 100644 src/test/mocks/silkBasket/silkBasketParsed.ts create mode 100644 src/test/mocks/silkBasket/silkBasketResponse.json create mode 100644 src/test/mocks/silkBasket/silkBasketWithMissingPricesParsed.ts create mode 100644 src/test/mocks/silkBasket/silkBasketWithOracleErrorParsed.ts create mode 100644 src/types/contracts/silkBasket/model.ts create mode 100644 src/types/contracts/silkBasket/response.ts create mode 100644 src/types/contracts/silkBasket/service.ts diff --git a/.changeset/spicy-boats-join.md b/.changeset/spicy-boats-join.md new file mode 100644 index 0000000..a94a6af --- /dev/null +++ b/.changeset/spicy-boats-join.md @@ -0,0 +1,5 @@ +--- +"@shadeprotocol/shadejs": minor +--- + +silk basket query diff --git a/src/contracts/definitions/silkBasket.test.ts b/src/contracts/definitions/silkBasket.test.ts new file mode 100644 index 0000000..2d1f7d2 --- /dev/null +++ b/src/contracts/definitions/silkBasket.test.ts @@ -0,0 +1,12 @@ +import { + test, + expect, +} from 'vitest'; +import { msgGetSilkBasket } from './silkBasket'; + +test('it tests the form of the silk basket message', () => { + const output = { + get_index_data: {}, + }; + expect(msgGetSilkBasket()).toStrictEqual(output); +}); diff --git a/src/contracts/definitions/silkBasket.ts b/src/contracts/definitions/silkBasket.ts new file mode 100644 index 0000000..3a27db1 --- /dev/null +++ b/src/contracts/definitions/silkBasket.ts @@ -0,0 +1,10 @@ +/** + * message for the getting the silk basket data + */ +const msgGetSilkBasket = () => ({ + get_index_data: {}, +}); + +export { + msgGetSilkBasket, +}; diff --git a/src/contracts/services/silkBasket.test.ts b/src/contracts/services/silkBasket.test.ts new file mode 100644 index 0000000..d01f6ca --- /dev/null +++ b/src/contracts/services/silkBasket.test.ts @@ -0,0 +1,257 @@ +import { + test, + expect, + vi, + beforeAll, + afterEach, +} from 'vitest'; +import silkBasketResponse from '~/test/mocks/silkBasket/silkBasketResponse.json'; +import { of } from 'rxjs'; +import { MinBlockHeightValidationOptions } from '~/types'; +import { + silkBasketParsed, + silkBasketParsedWithStatus, +} from '~/test/mocks/silkBasket/silkBasketParsed'; +import { batchSilkBasketOraclePricesResponse } from '~/test/mocks/silkBasket/batchSilkBasketOraclePricesResponse'; +import { batchSilkBasketWithMissingOraclePricesResponse } from '~/test/mocks/silkBasket/batchSilkBasketWithMissingOraclePricesResponse'; +import { silkBasketWithMissingPricesParsed } from '~/test/mocks/silkBasket/silkBasketWithMissingPricesParsed'; +import { batchSilkBasketWithOracleErrorResponse } from '~/test/mocks/silkBasket/batchSilkBasketWithOracleErrorResponse'; +import { silkBasketWithOracleErrorParsed } from '~/test/mocks/silkBasket/silkBasketWithOracleErrorParsed'; +import { batchSilkBasketWithBasketErrorResponse } from '~/test/mocks/silkBasket/batchSilkBasketWithBasketErrorResponse'; +import { SilkBasketBatchResponseItem } from '~/types/contracts/silkBasket/service'; +import { + parseSilkBasketAndPricesResponse, + parseSilkBasketAndPricesResponseFromQueryRouter, + querySilkBasket$, + querySilkBasket, +} from './silkBasket'; + +const batchQuery$ = vi.hoisted(() => vi.fn()); + +beforeAll(() => { + vi.mock('~/contracts/definitions/silkBasket', () => ({ + msgGetSilkBasket: vi.fn(() => 'MSG_GET_SILK_BASKET'), + })); + + vi.mock('~/contracts/services/batchQuery', () => ({ + batchQuery$, + })); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +test('it can parse the silk basket response', () => { + expect(parseSilkBasketAndPricesResponse({ + silkBasketResponse, + batchBasketPricesResponse: batchSilkBasketOraclePricesResponse[1], + silkBasketResponseBlockHeight: 1, + basketPricesResponseBlockHeight: 1, + })).toStrictEqual(silkBasketParsedWithStatus); +}); + +test('it can parse the silk basket response via the query router', () => { + expect(parseSilkBasketAndPricesResponseFromQueryRouter( + batchSilkBasketOraclePricesResponse, + )).toStrictEqual(silkBasketParsedWithStatus); +}); + +test('it can parse the silk basket response when prices are missing', () => { + expect(parseSilkBasketAndPricesResponseFromQueryRouter( + batchSilkBasketWithMissingOraclePricesResponse, + )).toStrictEqual(silkBasketWithMissingPricesParsed); +}); + +test('it can parse the silk basket response when the price query has an error', () => { + expect(parseSilkBasketAndPricesResponseFromQueryRouter( + batchSilkBasketWithOracleErrorResponse, + )).toStrictEqual(silkBasketWithOracleErrorParsed); +}); + +test('it can detect and throw error for silk basket info error', () => { + expect(() => parseSilkBasketAndPricesResponseFromQueryRouter( + batchSilkBasketWithBasketErrorResponse, + )).toThrowError('BASKET_ERROR_MESSAGE'); +}); +test('it can call silk basket info service', async () => { + const input = { + queryRouterContractAddress: 'QUERY_ROUTER_CONTRACT_ADDRESS', + queryRouterCodeHash: 'QUERY_ROUTER_CODE_HASH', + lcdEndpoint: 'LCD_ENDPOINT', + chainId: 'CHAIN_ID', + oracleContractAddress: 'ORACLE_CONTRACT_ADDRESS', + oracleCodeHash: 'ORACLE_CODE_HASH', + silkBasketExpectedOracleKeys: ['KEY_1, KEY_2'], + silkIndexOracleContractAddress: 'SILK_INDEX_ORACLE_CONTRACT_ADDRESS', + silkIndexOracleCodeHash: 'SILK_INDEX_ORACLE_CODE_HASH', + minBlockHeightValidationOptions: 'BLOCK_HEIGHT_VALIDATION_OPTIONS' as unknown as MinBlockHeightValidationOptions, + }; + // observables function + batchQuery$.mockReturnValueOnce(of(batchSilkBasketOraclePricesResponse)); + + let output; + querySilkBasket$(input).subscribe({ + next: (response) => { + output = response; + }, + }); + + const batchQueryInput = { + contractAddress: input.queryRouterContractAddress, + codeHash: input.queryRouterCodeHash, + lcdEndpoint: input.lcdEndpoint, + chainId: input.chainId, + queries: [{ + id: SilkBasketBatchResponseItem.BASKET, + contract: { + address: input.silkIndexOracleContractAddress, + codeHash: input.silkIndexOracleCodeHash, + }, + queryMsg: 'MSG_GET_SILK_BASKET', + }, + { + id: SilkBasketBatchResponseItem.PRICES, + contract: { + address: input.oracleContractAddress, + codeHash: input.oracleCodeHash, + }, + queryMsg: { + get_prices: { + keys: input.silkBasketExpectedOracleKeys, + }, + }, + }, + ], + minBlockHeightValidationOptions: input.minBlockHeightValidationOptions, + }; + expect(batchQuery$).toHaveBeenCalledWith(batchQueryInput); + + expect(output).toStrictEqual(silkBasketParsed); + + // async/await function + batchQuery$.mockReturnValueOnce(of(batchSilkBasketOraclePricesResponse)); + const response = await querySilkBasket(input); + expect(batchQuery$).toHaveBeenCalledWith(batchQueryInput); + expect(response).toStrictEqual(silkBasketParsed); +}); + +test('it can handle missing prices with a query retry', async () => { + const input = { + queryRouterContractAddress: 'QUERY_ROUTER_CONTRACT_ADDRESS', + queryRouterCodeHash: 'QUERY_ROUTER_CODE_HASH', + lcdEndpoint: 'LCD_ENDPOINT', + chainId: 'CHAIN_ID', + oracleContractAddress: 'ORACLE_CONTRACT_ADDRESS', + oracleCodeHash: 'ORACLE_CODE_HASH', + silkBasketExpectedOracleKeys: ['KEY_1, KEY_2'], + silkIndexOracleContractAddress: 'SILK_INDEX_ORACLE_CONTRACT_ADDRESS', + silkIndexOracleCodeHash: 'SILK_INDEX_ORACLE_CODE_HASH', + minBlockHeightValidationOptions: 'BLOCK_HEIGHT_VALIDATION_OPTIONS' as unknown as MinBlockHeightValidationOptions, + }; + // observables function + batchQuery$ + .mockReturnValueOnce(of(batchSilkBasketWithMissingOraclePricesResponse)) + .mockReturnValueOnce(of(batchSilkBasketOraclePricesResponse)); + + let output; + querySilkBasket$(input).subscribe({ + next: (response) => { + output = response; + }, + }); + + const firstPriceQueryKeys = ['KEY_1, KEY_2']; + const retryPriceQueryKeys = ['CAD', 'USD', 'JPY', 'BTC', 'EUR', 'XAU']; + + const batchQueryInput1 = { + contractAddress: input.queryRouterContractAddress, + codeHash: input.queryRouterCodeHash, + lcdEndpoint: input.lcdEndpoint, + chainId: input.chainId, + queries: [ + { + id: SilkBasketBatchResponseItem.BASKET, + contract: { + address: input.silkIndexOracleContractAddress, + codeHash: input.silkIndexOracleCodeHash, + }, + queryMsg: 'MSG_GET_SILK_BASKET', + }, + { + id: SilkBasketBatchResponseItem.PRICES, + contract: { + address: input.oracleContractAddress, + codeHash: input.oracleCodeHash, + }, + queryMsg: { + get_prices: { + keys: firstPriceQueryKeys, + }, + }, + }, + ], + minBlockHeightValidationOptions: input.minBlockHeightValidationOptions, + }; + + const batchQueryInput2 = { + contractAddress: input.queryRouterContractAddress, + codeHash: input.queryRouterCodeHash, + lcdEndpoint: input.lcdEndpoint, + chainId: input.chainId, + queries: [ + { + id: SilkBasketBatchResponseItem.BASKET, + contract: { + address: input.silkIndexOracleContractAddress, + codeHash: input.silkIndexOracleCodeHash, + }, + queryMsg: 'MSG_GET_SILK_BASKET', + }, + { + id: SilkBasketBatchResponseItem.PRICES, + contract: { + address: input.oracleContractAddress, + codeHash: input.oracleCodeHash, + }, + queryMsg: { + get_prices: { + keys: retryPriceQueryKeys, + }, + }, + }, + ], + minBlockHeightValidationOptions: input.minBlockHeightValidationOptions, + }; + + expect(batchQuery$).toHaveBeenNthCalledWith(1, batchQueryInput1); + expect(batchQuery$).toHaveBeenNthCalledWith(2, batchQueryInput2); + + expect(output).toStrictEqual(silkBasketParsed); +}); + +test('it can properly catch the silk basket error', async () => { + const input = { + queryRouterContractAddress: 'QUERY_ROUTER_CONTRACT_ADDRESS', + queryRouterCodeHash: 'QUERY_ROUTER_CODE_HASH', + lcdEndpoint: 'LCD_ENDPOINT', + chainId: 'CHAIN_ID', + oracleContractAddress: 'ORACLE_CONTRACT_ADDRESS', + oracleCodeHash: 'ORACLE_CODE_HASH', + silkBasketExpectedOracleKeys: ['KEY_1, KEY_2'], + silkIndexOracleContractAddress: 'SILK_INDEX_ORACLE_CONTRACT_ADDRESS', + silkIndexOracleCodeHash: 'SILK_INDEX_ORACLE_CODE_HASH', + minBlockHeightValidationOptions: 'BLOCK_HEIGHT_VALIDATION_OPTIONS' as unknown as MinBlockHeightValidationOptions, + }; + // observables function + batchQuery$.mockReturnValueOnce(of(batchSilkBasketWithBasketErrorResponse)); + + let error; + querySilkBasket$(input).subscribe({ + error: (err) => { + error = err.message; + }, + }); + + expect(error).toStrictEqual('BASKET_ERROR_MESSAGE'); +}); diff --git a/src/contracts/services/silkBasket.ts b/src/contracts/services/silkBasket.ts new file mode 100644 index 0000000..4b4ceb7 --- /dev/null +++ b/src/contracts/services/silkBasket.ts @@ -0,0 +1,328 @@ +import { SilkBasketResponse } from '~/types/contracts/silkBasket/response'; +import { SilkBasket, BasketItem, SilkBasketParsingStatus } from '~/types/contracts/silkBasket/model'; +import { convertCoinFromUDenom } from '~/lib/utils'; +import { + BatchQueryParsedResponse, + BatchItemResponseStatus, + MinBlockHeightValidationOptions, + BatchQueryParams, + OraclePricesResponse, + BatchQueryParsedResponseItem, +} from '~/types'; +import { msgGetSilkBasket } from '~/contracts/definitions/silkBasket'; +import { + map, + first, + firstValueFrom, + switchMap, + of, +} from 'rxjs'; +import { msgQueryOraclePrices } from '~/contracts/definitions'; +import { SilkBasketBatchResponseItem } from '~/types/contracts/silkBasket/service'; +import { batchQuery$ } from './batchQuery'; + +const ORACLE_NORMALIZATION_FACTOR = 18; + +/** + * parses the direct contract query into the data model + */ +const parseSilkBasketAndPricesResponse = ({ + silkBasketResponse, + batchBasketPricesResponse, + silkBasketResponseBlockHeight, + basketPricesResponseBlockHeight, +}:{ + silkBasketResponse: SilkBasketResponse, + // return the prices batch data here for error handling purposes + batchBasketPricesResponse: BatchQueryParsedResponseItem, + silkBasketResponseBlockHeight: number, + basketPricesResponseBlockHeight: number, +}): SilkBasket & { status: SilkBasketParsingStatus } => { + // error checking + + // block height validation + // data should be returned in a single batch with a single block height and + // never cause this error, but in case for any reason it does not, we will validate that here + // to ensure accuracy of the data when performing calculations of two different sources + if (silkBasketResponseBlockHeight !== basketPricesResponseBlockHeight) { + throw new Error('basket data and prices were returned from different block heights so data accuracy cannot be confirmed.'); + } + + const { basket } = silkBasketResponse; + + const silkPrice = convertCoinFromUDenom( + silkBasketResponse.peg.value, + ORACLE_NORMALIZATION_FACTOR, + ); + const silkInitialPrice = convertCoinFromUDenom( + silkBasketResponse.peg.target, + ORACLE_NORMALIZATION_FACTOR, + ); + + // initialize the return status to the success state which + // will be modified if a different state is encountered + let status = SilkBasketParsingStatus.SUCCESS; + + const basketItems: BasketItem[] = basket.map((item) => { + // for the prices error state we will return basket data without prices + if (batchBasketPricesResponse.status === BatchItemResponseStatus.ERROR) { + status = SilkBasketParsingStatus.PRICES_MISSING; + return { + symbol: item.symbol, + amount: item.weight.fixed, + price: undefined, + weight: { + initial: item.weight.initial, + current: undefined, + }, + }; + } + + // for non-error state, there's also a chance a price could be missing due to a + // change in the basket compared to the input oracle keys and so we will handle + // that case as well + const basketPricesResponse = batchBasketPricesResponse.response as OraclePricesResponse; + + const matchingPriceResponse = basketPricesResponse.find((price) => price.key === item.symbol); + // price is missing so return undefined on the + // calculated values. Query will need to be retried with modified oracle keys. + if (!matchingPriceResponse) { + status = SilkBasketParsingStatus.PRICES_MISSING; + return { + symbol: item.symbol, + amount: item.weight.fixed, + price: undefined, + weight: { + initial: item.weight.initial, + current: undefined, + }, + }; + } + + // confirmed price is available so we will return the price/weight calculated data + const itemPrice = convertCoinFromUDenom( + matchingPriceResponse.data.rate, + ORACLE_NORMALIZATION_FACTOR, + ); + const currentWeight = itemPrice.multipliedBy(item.weight.fixed).dividedBy(silkPrice); + return { + symbol: item.symbol, + amount: item.weight.fixed, + price: itemPrice.toString(), + weight: { + initial: item.weight.initial, + current: currentWeight.toString(), + }, + }; + }); + + return { + symbol: silkBasketResponse.symbol, + oracleRouterContract: { + address: silkBasketResponse.router.address, + codeHash: silkBasketResponse.router.code_hash, + }, + staleInterval: Number(silkBasketResponse.when_stale), + peg: { + value: silkPrice.toString(), + initialValue: silkInitialPrice.toString(), + isFrozen: silkBasketResponse.peg.frozen, + lastUpdatedAt: new Date(Number(silkBasketResponse.peg.last_updated) * 1000), + }, + basket: basketItems, + blockHeight: silkBasketResponseBlockHeight, + status, + }; +}; + +/** + * parses the silk basket and prices response via the query router + */ +function parseSilkBasketAndPricesResponseFromQueryRouter( + batchQueryResponse: BatchQueryParsedResponse, +) { + const responseCount = batchQueryResponse.length; + if (responseCount !== 2) { + throw new Error(`${responseCount} responses found, two responses are expected`); + } + + const silkBasketResponse = batchQueryResponse.find(( + responseItem, + ) => responseItem.id === SilkBasketBatchResponseItem.BASKET); + + // check if data exists + if (!silkBasketResponse) { + throw new Error('Silk basket response not found in query router response'); + } + // handle error state + if (silkBasketResponse.status === BatchItemResponseStatus.ERROR) { + throw new Error(silkBasketResponse.response); + } + + const pricesResponse = batchQueryResponse.find(( + responseItem, + ) => responseItem.id === SilkBasketBatchResponseItem.PRICES); + + // check if data exists + if (!pricesResponse) { + throw new Error('prices response not found in query router response'); + } + + // for price error state we will not throw an error here as there is still useful + // data to get out of the peg info that we would like to parse. + // Instead, we will pass the full batch data into the parser (which includes the + // error status) and handle the error downstream. + + return parseSilkBasketAndPricesResponse({ + silkBasketResponse: silkBasketResponse.response, + batchBasketPricesResponse: pricesResponse, + silkBasketResponseBlockHeight: silkBasketResponse.blockHeight, + basketPricesResponseBlockHeight: pricesResponse.blockHeight, + }); +} + +/** + * query the silk basket data + */ +function querySilkBasket$({ + queryRouterContractAddress, + queryRouterCodeHash, + lcdEndpoint, + chainId, + oracleContractAddress, + oracleCodeHash, + silkBasketExpectedOracleKeys, // if keys are known in advance it will + // increase the efficiency of the response, but even if keys are unknown or + // incorrectly passed in compared to the state of the basket, + // this service will self-correct using a retry on the actual keys found. + silkIndexOracleContractAddress, + silkIndexOracleCodeHash, + minBlockHeightValidationOptions, +}:{ + queryRouterContractAddress: string, + queryRouterCodeHash?: string, + lcdEndpoint?: string, + chainId?: string, + oracleContractAddress: string, + oracleCodeHash: string, + silkBasketExpectedOracleKeys?: string[] + silkIndexOracleContractAddress: string, + silkIndexOracleCodeHash: string, + minBlockHeightValidationOptions?: MinBlockHeightValidationOptions, +}) { + const basketQueryMsg :BatchQueryParams = { + id: SilkBasketBatchResponseItem.BASKET, + contract: { + address: silkIndexOracleContractAddress, + codeHash: silkIndexOracleCodeHash, + }, + queryMsg: msgGetSilkBasket(), + }; + + const pricesQueryMsg:BatchQueryParams = { + id: SilkBasketBatchResponseItem.PRICES, + contract: { + address: oracleContractAddress, + codeHash: oracleCodeHash, + }, + queryMsg: silkBasketExpectedOracleKeys + ? msgQueryOraclePrices(silkBasketExpectedOracleKeys) + : msgQueryOraclePrices([]), + }; + + return batchQuery$({ + contractAddress: queryRouterContractAddress, + codeHash: queryRouterCodeHash, + lcdEndpoint, + chainId, + queries: [basketQueryMsg, pricesQueryMsg], + minBlockHeightValidationOptions, + }).pipe( + map(parseSilkBasketAndPricesResponseFromQueryRouter), + switchMap((data) => { + if (data.status === SilkBasketParsingStatus.PRICES_MISSING) { + // retry with different oracle key array + const pricesQueryMsgWithNewKeys:BatchQueryParams = { + id: SilkBasketBatchResponseItem.PRICES, + contract: { + address: oracleContractAddress, + codeHash: oracleCodeHash, + }, + queryMsg: msgQueryOraclePrices(data.basket.map((item) => item.symbol)), + }; + return batchQuery$({ + contractAddress: queryRouterContractAddress, + codeHash: queryRouterCodeHash, + lcdEndpoint, + chainId, + queries: [basketQueryMsg, pricesQueryMsgWithNewKeys], + minBlockHeightValidationOptions, + }).pipe( + map(parseSilkBasketAndPricesResponseFromQueryRouter), + first(), + ); + } + return of(data); + }), + // remove the status property because this was only needed for + // handling missing prices + map((data) => { + const { status, ...otherProperties } = data; + const silkBasket: SilkBasket = { + ...otherProperties, + }; + return silkBasket; + }), + first(), + ); +} + +/** + * query the stability pool info + */ +function querySilkBasket({ + queryRouterContractAddress, + queryRouterCodeHash, + lcdEndpoint, + chainId, + oracleContractAddress, + oracleCodeHash, + silkBasketExpectedOracleKeys, // if keys are known in advance it will + // increase the efficiency of the response, but even if keys are unknown or + // incorrectly passed in compared to the state of the basket, + // this service will self-correct using a retry on the actual keys found. + silkIndexOracleContractAddress, + silkIndexOracleCodeHash, + minBlockHeightValidationOptions, +}:{ + queryRouterContractAddress: string, + queryRouterCodeHash?: string, + lcdEndpoint?: string, + chainId?: string, + oracleContractAddress: string, + oracleCodeHash: string, + silkBasketExpectedOracleKeys: string[] + silkIndexOracleContractAddress: string, + silkIndexOracleCodeHash: string, + minBlockHeightValidationOptions?: MinBlockHeightValidationOptions, +}) { + return firstValueFrom(querySilkBasket$({ + queryRouterContractAddress, + queryRouterCodeHash, + lcdEndpoint, + chainId, + oracleContractAddress, + oracleCodeHash, + silkBasketExpectedOracleKeys, + silkIndexOracleContractAddress, + silkIndexOracleCodeHash, + minBlockHeightValidationOptions, + })); +} + +export { + parseSilkBasketAndPricesResponse, + parseSilkBasketAndPricesResponseFromQueryRouter, + querySilkBasket$, + querySilkBasket, +}; diff --git a/src/test/mocks/silkBasket/batchSilkBasketOraclePricesResponse.ts b/src/test/mocks/silkBasket/batchSilkBasketOraclePricesResponse.ts new file mode 100644 index 0000000..81494a6 --- /dev/null +++ b/src/test/mocks/silkBasket/batchSilkBasketOraclePricesResponse.ts @@ -0,0 +1,126 @@ +import { BatchQueryParsedResponse } from '~/types'; +import { SilkBasketBatchResponseItem } from '~/types/contracts/silkBasket/service'; + +const batchSilkBasketOraclePricesResponse: BatchQueryParsedResponse = [ + { + id: SilkBasketBatchResponseItem.BASKET, + response: { + symbol: 'SILK Peg', + router: { + address: 'secret10n2xl5jmez6r9umtdrth78k0vwmce0l5m9f5dm', + code_hash: '32c4710842b97a526c243a68511b15f58d6e72a388af38a7221ff3244c754e91', + }, + when_stale: '3600', + peg: { + target: '1050000000000000000', + value: '1149881971000000000', + last_value: '1149881971000000000', + frozen: false, + last_updated: '1714053218', + }, + basket: [ + { + symbol: 'CAD', + weight: { + initial: '0.077167854630036405', + fixed: '0.107192209875052435', + }, + }, + { + symbol: 'USD', + weight: { + initial: '0.337301580837853137', + fixed: '0.353843538457366364', + }, + }, + { + symbol: 'JPY', + weight: { + initial: '0.103589679459286984', + fixed: '15.684456395164345505', + }, + }, + { + symbol: 'BTC', + weight: { + initial: '0.068084956080984917', + fixed: '0.000002334689261109', + }, + }, + { + symbol: 'EUR', + weight: { + initial: '0.232086119065448055', + fixed: '0.223108470434723066', + }, + }, + { + symbol: 'XAU', + weight: { + initial: '0.181769809926390499', + fixed: '0.000099333104038645', + }, + }, + ], + }, + blockHeight: 1, + }, + { + id: SilkBasketBatchResponseItem.PRICES, + response: [ + { + key: 'CAD', + data: { + rate: '731678675000000000', + last_updated_base: 1714070751, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'BTC', + data: { + rate: '64526100000000000000000', + last_updated_base: 1714070794, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'JPY', + data: { + rate: '6425558000000000', + last_updated_base: 1714070751, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'USD', + data: { + rate: '1000000000000000000', + last_updated_base: 18446744073709552000, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'EUR', + data: { + rate: '1072833942000000000', + last_updated_base: 1714070751, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'XAU', + data: { + rate: '2332043324700000000000', + last_updated_base: 1714070751, + last_updated_quote: 18446744073709552000, + }, + }, + ], + blockHeight: 1, + }, +]; + +export { + batchSilkBasketOraclePricesResponse, +}; diff --git a/src/test/mocks/silkBasket/batchSilkBasketWithBasketErrorResponse.ts b/src/test/mocks/silkBasket/batchSilkBasketWithBasketErrorResponse.ts new file mode 100644 index 0000000..c65de6a --- /dev/null +++ b/src/test/mocks/silkBasket/batchSilkBasketWithBasketErrorResponse.ts @@ -0,0 +1,70 @@ +import { BatchItemResponseStatus, BatchQueryParsedResponse } from '~/types'; +import { SilkBasketBatchResponseItem } from '~/types/contracts/silkBasket/service'; + +const batchSilkBasketWithBasketErrorResponse: BatchQueryParsedResponse = [ + { + id: SilkBasketBatchResponseItem.BASKET, + response: 'BASKET_ERROR_MESSAGE', + blockHeight: 1, + status: BatchItemResponseStatus.ERROR, + }, + { + id: SilkBasketBatchResponseItem.PRICES, + response: [ + { + key: 'CAD', + data: { + rate: '731678675000000000', + last_updated_base: 1714070751, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'BTC', + data: { + rate: '64526100000000000000000', + last_updated_base: 1714070794, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'JPY', + data: { + rate: '6425558000000000', + last_updated_base: 1714070751, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'USD', + data: { + rate: '1000000000000000000', + last_updated_base: 18446744073709552000, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'EUR', + data: { + rate: '1072833942000000000', + last_updated_base: 1714070751, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'XAU', + data: { + rate: '2332043324700000000000', + last_updated_base: 1714070751, + last_updated_quote: 18446744073709552000, + }, + }, + ], + blockHeight: 1, + status: BatchItemResponseStatus.SUCCESS, + }, +]; + +export { + batchSilkBasketWithBasketErrorResponse, +}; diff --git a/src/test/mocks/silkBasket/batchSilkBasketWithMissingOraclePricesResponse.ts b/src/test/mocks/silkBasket/batchSilkBasketWithMissingOraclePricesResponse.ts new file mode 100644 index 0000000..18cb9c8 --- /dev/null +++ b/src/test/mocks/silkBasket/batchSilkBasketWithMissingOraclePricesResponse.ts @@ -0,0 +1,118 @@ +import { BatchQueryParsedResponse } from '~/types'; +import { SilkBasketBatchResponseItem } from '~/types/contracts/silkBasket/service'; + +const batchSilkBasketWithMissingOraclePricesResponse: BatchQueryParsedResponse = [ + { + id: SilkBasketBatchResponseItem.BASKET, + response: { + symbol: 'SILK Peg', + router: { + address: 'secret10n2xl5jmez6r9umtdrth78k0vwmce0l5m9f5dm', + code_hash: '32c4710842b97a526c243a68511b15f58d6e72a388af38a7221ff3244c754e91', + }, + when_stale: '3600', + peg: { + target: '1050000000000000000', + value: '1149881971000000000', + last_value: '1149881971000000000', + frozen: false, + last_updated: '1714053218', + }, + basket: [ + { + symbol: 'CAD', + weight: { + initial: '0.077167854630036405', + fixed: '0.107192209875052435', + }, + }, + { + symbol: 'USD', + weight: { + initial: '0.337301580837853137', + fixed: '0.353843538457366364', + }, + }, + { + symbol: 'JPY', + weight: { + initial: '0.103589679459286984', + fixed: '15.684456395164345505', + }, + }, + { + symbol: 'BTC', + weight: { + initial: '0.068084956080984917', + fixed: '0.000002334689261109', + }, + }, + { + symbol: 'EUR', + weight: { + initial: '0.232086119065448055', + fixed: '0.223108470434723066', + }, + }, + { + symbol: 'XAU', + weight: { + initial: '0.181769809926390499', + fixed: '0.000099333104038645', + }, + }, + ], + }, + blockHeight: 1, + }, + { + id: SilkBasketBatchResponseItem.PRICES, + response: [ + { + key: 'CAD', + data: { + rate: '731678675000000000', + last_updated_base: 1714070751, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'BTC', + data: { + rate: '64526100000000000000000', + last_updated_base: 1714070794, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'JPY', + data: { + rate: '6425558000000000', + last_updated_base: 1714070751, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'USD', + data: { + rate: '1000000000000000000', + last_updated_base: 18446744073709552000, + last_updated_quote: 18446744073709552000, + }, + }, + { + key: 'EUR', + data: { + rate: '1072833942000000000', + last_updated_base: 1714070751, + last_updated_quote: 18446744073709552000, + }, + }, + ], + blockHeight: 1, + }, +]; + +export { + batchSilkBasketWithMissingOraclePricesResponse, +}; diff --git a/src/test/mocks/silkBasket/batchSilkBasketWithOracleErrorResponse.ts b/src/test/mocks/silkBasket/batchSilkBasketWithOracleErrorResponse.ts new file mode 100644 index 0000000..2ebdf5a --- /dev/null +++ b/src/test/mocks/silkBasket/batchSilkBasketWithOracleErrorResponse.ts @@ -0,0 +1,78 @@ +import { BatchItemResponseStatus, BatchQueryParsedResponse } from '~/types'; +import { SilkBasketBatchResponseItem } from '~/types/contracts/silkBasket/service'; + +const batchSilkBasketWithOracleErrorResponse: BatchQueryParsedResponse = [ + { + id: SilkBasketBatchResponseItem.BASKET, + response: { + symbol: 'SILK Peg', + router: { + address: 'secret10n2xl5jmez6r9umtdrth78k0vwmce0l5m9f5dm', + code_hash: '32c4710842b97a526c243a68511b15f58d6e72a388af38a7221ff3244c754e91', + }, + when_stale: '3600', + peg: { + target: '1050000000000000000', + value: '1149881971000000000', + last_value: '1149881971000000000', + frozen: false, + last_updated: '1714053218', + }, + basket: [ + { + symbol: 'CAD', + weight: { + initial: '0.077167854630036405', + fixed: '0.107192209875052435', + }, + }, + { + symbol: 'USD', + weight: { + initial: '0.337301580837853137', + fixed: '0.353843538457366364', + }, + }, + { + symbol: 'JPY', + weight: { + initial: '0.103589679459286984', + fixed: '15.684456395164345505', + }, + }, + { + symbol: 'BTC', + weight: { + initial: '0.068084956080984917', + fixed: '0.000002334689261109', + }, + }, + { + symbol: 'EUR', + weight: { + initial: '0.232086119065448055', + fixed: '0.223108470434723066', + }, + }, + { + symbol: 'XAU', + weight: { + initial: '0.181769809926390499', + fixed: '0.000099333104038645', + }, + }, + ], + }, + blockHeight: 1, + }, + { + id: SilkBasketBatchResponseItem.PRICES, + response: 'PRICE_ERROR_MESSAGE', + blockHeight: 1, + status: BatchItemResponseStatus.ERROR, + }, +]; + +export { + batchSilkBasketWithOracleErrorResponse, +}; diff --git a/src/test/mocks/silkBasket/pricesResponse.json b/src/test/mocks/silkBasket/pricesResponse.json new file mode 100644 index 0000000..4030d7d --- /dev/null +++ b/src/test/mocks/silkBasket/pricesResponse.json @@ -0,0 +1,50 @@ +[ + { + "key": "CAD", + "data": { + "rate": "731678675000000000", + "last_updated_base": 1714070751, + "last_updated_quote": 18446744073709552000 + } + }, + { + "key": "BTC", + "data": { + "rate": "64526100000000000000000", + "last_updated_base": 1714070794, + "last_updated_quote": 18446744073709552000 + } + }, + { + "key": "JPY", + "data": { + "rate": "6425558000000000", + "last_updated_base": 1714070751, + "last_updated_quote": 18446744073709552000 + } + }, + { + "key": "USD", + "data": { + "rate": "1000000000000000000", + "last_updated_base": 18446744073709552000, + "last_updated_quote": 18446744073709552000 + } + }, + { + "key": "EUR", + "data": { + "rate": "1072833942000000000", + "last_updated_base": 1714070751, + "last_updated_quote": 18446744073709552000 + } + }, + { + "key": "XAU", + "data": { + "rate": "2332043324700000000000", + "last_updated_base": 1714070751, + "last_updated_quote": 18446744073709552000 + } + } +] diff --git a/src/test/mocks/silkBasket/silkBasketParsed.ts b/src/test/mocks/silkBasket/silkBasketParsed.ts new file mode 100644 index 0000000..af6f6af --- /dev/null +++ b/src/test/mocks/silkBasket/silkBasketParsed.ts @@ -0,0 +1,147 @@ +import { SilkBasket, SilkBasketParsingStatus } from '~/types/contracts/silkBasket/model'; + +const silkBasketParsedWithStatus: SilkBasket & { status: SilkBasketParsingStatus } = { + symbol: 'SILK Peg', + oracleRouterContract: { + address: 'secret10n2xl5jmez6r9umtdrth78k0vwmce0l5m9f5dm', + codeHash: '32c4710842b97a526c243a68511b15f58d6e72a388af38a7221ff3244c754e91', + }, + staleInterval: 3600, + peg: { + value: '1.149881971', + initialValue: '1.05', + isFrozen: false, + lastUpdatedAt: new Date(1714053218000), + }, + basket: [{ + symbol: 'CAD', + amount: '0.107192209875052435', + price: '0.731678675', + weight: { + current: '0.068207221323326828', + initial: '0.077167854630036405', + }, + }, + { + symbol: 'USD', + amount: '0.353843538457366364', + price: '1', + weight: { + current: '0.307721616114778065', + initial: '0.337301580837853137', + }, + }, + { + symbol: 'JPY', + amount: '15.684456395164345505', + price: '0.006425558', + weight: { + current: '0.087644981665339478', + initial: '0.103589679459286984', + }, + }, + { + symbol: 'BTC', + amount: '0.000002334689261109', + price: '64526.1', + weight: { + current: '0.131012048654205263', + initial: '0.068084956080984917', + }, + }, + { + symbol: 'EUR', + amount: '0.223108470434723066', + price: '1.072833942', + weight: { + current: '0.20815905098669853', + initial: '0.232086119065448055', + }, + }, + { + symbol: 'XAU', + amount: '0.000099333104038645', + price: '2332.0433247', + weight: { + current: '0.201454677990644557', + initial: '0.181769809926390499', + }, + }], + blockHeight: 1, + status: SilkBasketParsingStatus.SUCCESS, +}; + +const silkBasketParsed: SilkBasket = { + symbol: 'SILK Peg', + oracleRouterContract: { + address: 'secret10n2xl5jmez6r9umtdrth78k0vwmce0l5m9f5dm', + codeHash: '32c4710842b97a526c243a68511b15f58d6e72a388af38a7221ff3244c754e91', + }, + staleInterval: 3600, + peg: { + value: '1.149881971', + initialValue: '1.05', + isFrozen: false, + lastUpdatedAt: new Date(1714053218000), + }, + basket: [{ + symbol: 'CAD', + amount: '0.107192209875052435', + price: '0.731678675', + weight: { + current: '0.068207221323326828', + initial: '0.077167854630036405', + }, + }, + { + symbol: 'USD', + amount: '0.353843538457366364', + price: '1', + weight: { + current: '0.307721616114778065', + initial: '0.337301580837853137', + }, + }, + { + symbol: 'JPY', + amount: '15.684456395164345505', + price: '0.006425558', + weight: { + current: '0.087644981665339478', + initial: '0.103589679459286984', + }, + }, + { + symbol: 'BTC', + amount: '0.000002334689261109', + price: '64526.1', + weight: { + current: '0.131012048654205263', + initial: '0.068084956080984917', + }, + }, + { + symbol: 'EUR', + amount: '0.223108470434723066', + price: '1.072833942', + weight: { + current: '0.20815905098669853', + initial: '0.232086119065448055', + }, + }, + { + symbol: 'XAU', + amount: '0.000099333104038645', + price: '2332.0433247', + weight: { + current: '0.201454677990644557', + initial: '0.181769809926390499', + }, + }], + blockHeight: 1, +}; + +export { + silkBasketParsedWithStatus, + silkBasketParsed, +}; diff --git a/src/test/mocks/silkBasket/silkBasketResponse.json b/src/test/mocks/silkBasket/silkBasketResponse.json new file mode 100644 index 0000000..6b6da92 --- /dev/null +++ b/src/test/mocks/silkBasket/silkBasketResponse.json @@ -0,0 +1,59 @@ +{ + "symbol": "SILK Peg", + "router": { + "address": "secret10n2xl5jmez6r9umtdrth78k0vwmce0l5m9f5dm", + "code_hash": "32c4710842b97a526c243a68511b15f58d6e72a388af38a7221ff3244c754e91" + }, + "when_stale": "3600", + "peg": { + "target": "1050000000000000000", + "value": "1149881971000000000", + "last_value": "1149881971000000000", + "frozen": false, + "last_updated": "1714053218" + }, + "basket": [ + { + "symbol": "CAD", + "weight": { + "initial": "0.077167854630036405", + "fixed": "0.107192209875052435" + } + }, + { + "symbol": "USD", + "weight": { + "initial": "0.337301580837853137", + "fixed": "0.353843538457366364" + } + }, + { + "symbol": "JPY", + "weight": { + "initial": "0.103589679459286984", + "fixed": "15.684456395164345505" + } + }, + { + "symbol": "BTC", + "weight": { + "initial": "0.068084956080984917", + "fixed": "0.000002334689261109" + } + }, + { + "symbol": "EUR", + "weight": { + "initial": "0.232086119065448055", + "fixed": "0.223108470434723066" + } + }, + { + "symbol": "XAU", + "weight": { + "initial": "0.181769809926390499", + "fixed": "0.000099333104038645" + } + } + ] +} diff --git a/src/test/mocks/silkBasket/silkBasketWithMissingPricesParsed.ts b/src/test/mocks/silkBasket/silkBasketWithMissingPricesParsed.ts new file mode 100644 index 0000000..6783550 --- /dev/null +++ b/src/test/mocks/silkBasket/silkBasketWithMissingPricesParsed.ts @@ -0,0 +1,76 @@ +import { SilkBasket, SilkBasketParsingStatus } from '~/types/contracts/silkBasket/model'; + +const silkBasketWithMissingPricesParsed: SilkBasket & { status: SilkBasketParsingStatus } = { + symbol: 'SILK Peg', + oracleRouterContract: { + address: 'secret10n2xl5jmez6r9umtdrth78k0vwmce0l5m9f5dm', + codeHash: '32c4710842b97a526c243a68511b15f58d6e72a388af38a7221ff3244c754e91', + }, + staleInterval: 3600, + peg: { + value: '1.149881971', + initialValue: '1.05', + isFrozen: false, + lastUpdatedAt: new Date(1714053218000), + }, + basket: [{ + symbol: 'CAD', + amount: '0.107192209875052435', + price: '0.731678675', + weight: { + current: '0.068207221323326828', + initial: '0.077167854630036405', + }, + }, + { + symbol: 'USD', + amount: '0.353843538457366364', + price: '1', + weight: { + current: '0.307721616114778065', + initial: '0.337301580837853137', + }, + }, + { + symbol: 'JPY', + amount: '15.684456395164345505', + price: '0.006425558', + weight: { + current: '0.087644981665339478', + initial: '0.103589679459286984', + }, + }, + { + symbol: 'BTC', + amount: '0.000002334689261109', + price: '64526.1', + weight: { + current: '0.131012048654205263', + initial: '0.068084956080984917', + }, + }, + { + symbol: 'EUR', + amount: '0.223108470434723066', + price: '1.072833942', + weight: { + current: '0.20815905098669853', + initial: '0.232086119065448055', + }, + }, + { + symbol: 'XAU', + amount: '0.000099333104038645', + price: undefined, + weight: { + current: undefined, + initial: '0.181769809926390499', + }, + }], + blockHeight: 1, + status: SilkBasketParsingStatus.PRICES_MISSING, +}; + +export { + silkBasketWithMissingPricesParsed, +}; diff --git a/src/test/mocks/silkBasket/silkBasketWithOracleErrorParsed.ts b/src/test/mocks/silkBasket/silkBasketWithOracleErrorParsed.ts new file mode 100644 index 0000000..e5221da --- /dev/null +++ b/src/test/mocks/silkBasket/silkBasketWithOracleErrorParsed.ts @@ -0,0 +1,76 @@ +import { SilkBasket, SilkBasketParsingStatus } from '~/types/contracts/silkBasket/model'; + +const silkBasketWithOracleErrorParsed: SilkBasket & { status: SilkBasketParsingStatus } = { + symbol: 'SILK Peg', + oracleRouterContract: { + address: 'secret10n2xl5jmez6r9umtdrth78k0vwmce0l5m9f5dm', + codeHash: '32c4710842b97a526c243a68511b15f58d6e72a388af38a7221ff3244c754e91', + }, + staleInterval: 3600, + peg: { + value: '1.149881971', + initialValue: '1.05', + isFrozen: false, + lastUpdatedAt: new Date(1714053218000), + }, + basket: [{ + symbol: 'CAD', + amount: '0.107192209875052435', + price: undefined, + weight: { + current: undefined, + initial: '0.077167854630036405', + }, + }, + { + symbol: 'USD', + amount: '0.353843538457366364', + price: undefined, + weight: { + current: undefined, + initial: '0.337301580837853137', + }, + }, + { + symbol: 'JPY', + amount: '15.684456395164345505', + price: undefined, + weight: { + current: undefined, + initial: '0.103589679459286984', + }, + }, + { + symbol: 'BTC', + amount: '0.000002334689261109', + price: undefined, + weight: { + current: undefined, + initial: '0.068084956080984917', + }, + }, + { + symbol: 'EUR', + amount: '0.223108470434723066', + price: undefined, + weight: { + current: undefined, + initial: '0.232086119065448055', + }, + }, + { + symbol: 'XAU', + amount: '0.000099333104038645', + price: undefined, + weight: { + current: undefined, + initial: '0.181769809926390499', + }, + }], + blockHeight: 1, + status: SilkBasketParsingStatus.PRICES_MISSING, +}; + +export { + silkBasketWithOracleErrorParsed, +}; diff --git a/src/types/contracts/silkBasket/model.ts b/src/types/contracts/silkBasket/model.ts new file mode 100644 index 0000000..36266b1 --- /dev/null +++ b/src/types/contracts/silkBasket/model.ts @@ -0,0 +1,39 @@ +import { Contract } from '~/types/contracts/shared'; + +enum SilkBasketParsingStatus { + PRICES_MISSING = 'prices_missing', + SUCCESS = 'success,' +} + +type BasketItem = { + symbol: string, + amount: string, + price?: string, + weight: { + initial: string, + current?: string, + } +} + +type SilkBasket = { + symbol: string, + oracleRouterContract: Contract, + staleInterval: number, + peg: { + value: string, // current value of the peg + initialValue: string, // starting value of the peg + isFrozen: boolean, + lastUpdatedAt: Date, + }, + basket: BasketItem[], + blockHeight: number, +} + +export type { + SilkBasket, + BasketItem, +}; + +export { + SilkBasketParsingStatus, +}; diff --git a/src/types/contracts/silkBasket/response.ts b/src/types/contracts/silkBasket/response.ts new file mode 100644 index 0000000..73e0cc0 --- /dev/null +++ b/src/types/contracts/silkBasket/response.ts @@ -0,0 +1,27 @@ +import { ContractData } from '~/types/contracts/shared'; + +type BasketResponseItem = { + symbol: string, + weight: { + initial: string, + fixed: string, + } +} + +type SilkBasketResponse = { + symbol: string, + router: ContractData, + when_stale: string, + peg: { + target: string, + value: string, + last_value: string, + frozen: boolean, + last_updated: string, + }, + basket: BasketResponseItem[], +} + +export type { + SilkBasketResponse, +}; diff --git a/src/types/contracts/silkBasket/service.ts b/src/types/contracts/silkBasket/service.ts new file mode 100644 index 0000000..341a6ab --- /dev/null +++ b/src/types/contracts/silkBasket/service.ts @@ -0,0 +1,8 @@ +enum SilkBasketBatchResponseItem { + BASKET='basket', + PRICES='prices' +} + +export { + SilkBasketBatchResponseItem, +}; From dbf19454819edf6540825f37831fb4efbdbf4ac1 Mon Sep 17 00:00:00 2001 From: Austin Date: Fri, 26 Apr 2024 14:41:47 -0500 Subject: [PATCH 2/3] docs: update docs for silk basket query --- docs/.vitepress/config.ts | 1 + docs/contracts.md | 1 + docs/queries/silk.md | 140 +++++++++++++++++++++++++++ src/contracts/services/silkBasket.ts | 24 ++--- 4 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 docs/queries/silk.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index b2b8ef7..dbeb7e7 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -31,6 +31,7 @@ export default defineConfig({ { text: 'stkd-SCRT', link: '/queries/derivativeScrt' }, { text: 'Shade Staking', link: '/queries/shadeStaking' }, { text: 'Batch Query', link: '/queries/batch-query' }, + { text: 'Silk', link: '/queries/silk' }, { text: 'Snip20', link: '/queries/snip20' }, ] }, diff --git a/docs/contracts.md b/docs/contracts.md index 1f27673..f697e76 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -15,6 +15,7 @@ This page contains a list of deployed contracts. | Lend Vault Registry (V2) | secret1qxk2scacpgj2mmm0af60674afl9e6qneg7yuny | ac5d501827d9a337a618ca493fcbf1323b20771378774a6bf466cb66361bf021 | | Lend Vault Registry (V3) | secret1wj2czeeknya2n6jag7kpfxlm28dw7q96dgqmfs | d837f716de3732a4118fbcb6d4cd0ef1d84ee83fef924f27b7c2a821f8528b39 | | Lend Stability Pool | secret1wdxqz26acf2e6rsac8007pd53ak7n8tgeqr46w | 4dcdce6a2f88ef2912b9988119b345b096909aa4ba3881eff19358d983c40210 | +| Index Oracle (SILK) | secret1552yh3rplmyrjwhcxrq0egg35uy6zwjtszecf0 | 8d2b439383091ecb7806757a2b202e0056e542ade67951a0d5c352e74ce416cc | ::: tip The ShadeSwap pairs contracts are accessible via the factory registered pairs query. diff --git a/docs/queries/silk.md b/docs/queries/silk.md new file mode 100644 index 0000000..ba3dd7b --- /dev/null +++ b/docs/queries/silk.md @@ -0,0 +1,140 @@ +# Silk Peg Examples + +This page demonstrates how to query the Silk peg and information about the basket of currencies that make up the Silk peg. + +## Silk Basket +Query info for the silk basket + +**input** +```ts +/** + * query the silk basket data + * + * silkBasketExpectedOracleKeys is set to optional because if oracle keys are + * known in advance it will increase the efficiency of the response, but even if + * keys are unknown or incorrectly passed in compared to the true state of the + * basket this query will self-correct using a retry on the actual keys found. + */ +function querySilkBasket({ + queryRouterContractAddress, + queryRouterCodeHash, + lcdEndpoint, + chainId, + oracleContractAddress, + oracleCodeHash, + silkBasketExpectedOracleKeys, + silkIndexOracleContractAddress, + silkIndexOracleCodeHash, + minBlockHeightValidationOptions, +}:{ + queryRouterContractAddress: string, + queryRouterCodeHash?: string, + lcdEndpoint?: string, + chainId?: string, + oracleContractAddress: string, + oracleCodeHash: string, + silkBasketExpectedOracleKeys?: string[] + silkIndexOracleContractAddress: string, + silkIndexOracleCodeHash: string, + minBlockHeightValidationOptions?: MinBlockHeightValidationOptions, +}): Promise +``` + +**output** +```ts +type SilkBasket = { + symbol: string, // silk peg oracle key + oracleRouterContract: Contract, + staleInterval: number, // seconds before stale oracle would cause a peg freeze. + peg: { + value: string, // current value of the peg + initialValue: string, // starting value of the peg + isFrozen: boolean, + lastUpdatedAt: Date, + }, + basket: BasketItem[], + blockHeight: number, +} + +type BasketItem = { + symbol: string, // oracle key + amount: string, // fixed amount of the currency that makes up the peg + price?: string, // price of the currency + weight: { + initial: string, // expressed as decimal percent + current?: string, + } +} +``` + +```md +{ + symbol: 'SILK Peg', + oracleRouterContract: { + address: 'secret10n2xl5jmez6r9umtdrth78k0vwmce0l5m9f5dm', + codeHash: '32c4710842b97a526c243a68511b15f58d6e72a388af38a7221ff3244c754e91', + }, + staleInterval: 3600, + peg: { + value: '1.149881971', + initialValue: '1.05', + isFrozen: false, + lastUpdatedAt: new Date(1714053218000), + }, + basket: [{ + symbol: 'CAD', + amount: '0.107192209875052435', + price: '0.731678675', + weight: { + current: '0.068207221323326828', + initial: '0.077167854630036405', + }, + }, + { + symbol: 'USD', + amount: '0.353843538457366364', + price: '1', + weight: { + current: '0.307721616114778065', + initial: '0.337301580837853137', + }, + }, + { + symbol: 'JPY', + amount: '15.684456395164345505', + price: '0.006425558', + weight: { + current: '0.087644981665339478', + initial: '0.103589679459286984', + }, + }, + { + symbol: 'BTC', + amount: '0.000002334689261109', + price: '64526.1', + weight: { + current: '0.131012048654205263', + initial: '0.068084956080984917', + }, + }, + { + symbol: 'EUR', + amount: '0.223108470434723066', + price: '1.072833942', + weight: { + current: '0.20815905098669853', + initial: '0.232086119065448055', + }, + }, + { + symbol: 'XAU', + amount: '0.000099333104038645', + price: '2332.0433247', + weight: { + current: '0.201454677990644557', + initial: '0.181769809926390499', + }, + }], + blockHeight: 123456789, +} +``` \ No newline at end of file diff --git a/src/contracts/services/silkBasket.ts b/src/contracts/services/silkBasket.ts index 4b4ceb7..836d440 100644 --- a/src/contracts/services/silkBasket.ts +++ b/src/contracts/services/silkBasket.ts @@ -24,7 +24,10 @@ import { batchQuery$ } from './batchQuery'; const ORACLE_NORMALIZATION_FACTOR = 18; /** - * parses the direct contract query into the data model + * parses the silk basket individual contract responses from the queries + * into the shared data model. + * This includes adding a parsing status to the output type for use in downstream + * error/retry handling */ const parseSilkBasketAndPricesResponse = ({ silkBasketResponse, @@ -33,13 +36,12 @@ const parseSilkBasketAndPricesResponse = ({ basketPricesResponseBlockHeight, }:{ silkBasketResponse: SilkBasketResponse, - // return the prices batch data here for error handling purposes + // return the prices batch data here instead of raw response + // for error handling purposes batchBasketPricesResponse: BatchQueryParsedResponseItem, silkBasketResponseBlockHeight: number, basketPricesResponseBlockHeight: number, }): SilkBasket & { status: SilkBasketParsingStatus } => { - // error checking - // block height validation // data should be returned in a single batch with a single block height and // never cause this error, but in case for any reason it does not, we will validate that here @@ -168,8 +170,8 @@ function parseSilkBasketAndPricesResponseFromQueryRouter( throw new Error('prices response not found in query router response'); } - // for price error state we will not throw an error here as there is still useful - // data to get out of the peg info that we would like to parse. + // for price error state we will not throw an error like we did with the basket + // data as there is still useful data to get out of the peg info that we would like to parse. // Instead, we will pass the full batch data into the parser (which includes the // error status) and handle the error downstream. @@ -191,7 +193,7 @@ function querySilkBasket$({ chainId, oracleContractAddress, oracleCodeHash, - silkBasketExpectedOracleKeys, // if keys are known in advance it will + silkBasketExpectedOracleKeys = [], // if keys are known in advance it will // increase the efficiency of the response, but even if keys are unknown or // incorrectly passed in compared to the state of the basket, // this service will self-correct using a retry on the actual keys found. @@ -225,9 +227,7 @@ function querySilkBasket$({ address: oracleContractAddress, codeHash: oracleCodeHash, }, - queryMsg: silkBasketExpectedOracleKeys - ? msgQueryOraclePrices(silkBasketExpectedOracleKeys) - : msgQueryOraclePrices([]), + queryMsg: msgQueryOraclePrices(silkBasketExpectedOracleKeys), }; return batchQuery$({ @@ -278,7 +278,7 @@ function querySilkBasket$({ } /** - * query the stability pool info + * query the silk basket data */ function querySilkBasket({ queryRouterContractAddress, @@ -301,7 +301,7 @@ function querySilkBasket({ chainId?: string, oracleContractAddress: string, oracleCodeHash: string, - silkBasketExpectedOracleKeys: string[] + silkBasketExpectedOracleKeys?: string[] silkIndexOracleContractAddress: string, silkIndexOracleCodeHash: string, minBlockHeightValidationOptions?: MinBlockHeightValidationOptions, From e0ffc1ff0fd17952cba06ec37069c8f17ae00c0c Mon Sep 17 00:00:00 2001 From: Austin Date: Fri, 26 Apr 2024 15:46:56 -0500 Subject: [PATCH 3/3] docs: contract table markdown formatting --- docs/contracts.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/contracts.md b/docs/contracts.md index f697e76..8031d0f 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -3,19 +3,19 @@ This page contains a list of deployed contracts. ## Mainnet -| Contract | Address | Code Hash | -|-------------------- |----------------------------------------------- |------------------------------------------------------------------ | -| ShadeSwap Factory | secret1ja0hcwvy76grqkpgwznxukgd7t8a8anmmx05pp | 2ad4ed2a4a45fd6de3daca9541ba82c26bb66c76d1c3540de39b509abd26538e | -| ShadeSwap Router | secret1pjhdug87nxzv0esxasmeyfsucaj98pw4334wyc | 448e3f6d801e453e838b7a5fbaa4dd93b84d0f1011245f0d5745366dadaf3e85 | -| Batch Query Router | secret15mkmad8ac036v4nrpcc7nk8wyr578egt077syt | 1c7e86ba4fdb6760e70bf08a7df7f44b53eb0b23290e3e69ca96140810d4f432 | -| Oracle | secret10n2xl5jmez6r9umtdrth78k0vwmce0l5m9f5dm | 32c4710842b97a526c243a68511b15f58d6e72a388af38a7221ff3244c754e91 | -| stkd-scrt | secret1k6u0cy4feepm6pehnz804zmwakuwdapm69tuc4 | f6be719b3c6feb498d3554ca0398eb6b7e7db262acb33f84a8f12106da6bbb09 | -| Shade Staking | secret1y6px5x7jzrk8hyvy67f06ytn8v0jwculypwxws | 2a1ae7fd2be82931cb11d0ce82b2e243507f2006074e2f316da661beb1abe3c3 | +| Contract | Address | Code Hash | +|----------------------------- |----------------------------------------------- |------------------------------------------------------------------ | +| ShadeSwap Factory | secret1ja0hcwvy76grqkpgwznxukgd7t8a8anmmx05pp | 2ad4ed2a4a45fd6de3daca9541ba82c26bb66c76d1c3540de39b509abd26538e | +| ShadeSwap Router | secret1pjhdug87nxzv0esxasmeyfsucaj98pw4334wyc | 448e3f6d801e453e838b7a5fbaa4dd93b84d0f1011245f0d5745366dadaf3e85 | +| Batch Query Router | secret15mkmad8ac036v4nrpcc7nk8wyr578egt077syt | 1c7e86ba4fdb6760e70bf08a7df7f44b53eb0b23290e3e69ca96140810d4f432 | +| Oracle | secret10n2xl5jmez6r9umtdrth78k0vwmce0l5m9f5dm | 32c4710842b97a526c243a68511b15f58d6e72a388af38a7221ff3244c754e91 | +| stkd-scrt | secret1k6u0cy4feepm6pehnz804zmwakuwdapm69tuc4 | f6be719b3c6feb498d3554ca0398eb6b7e7db262acb33f84a8f12106da6bbb09 | +| Shade Staking | secret1y6px5x7jzrk8hyvy67f06ytn8v0jwculypwxws | 2a1ae7fd2be82931cb11d0ce82b2e243507f2006074e2f316da661beb1abe3c3 | | Lend Vault Registry (V1) | secret18y86hldtdp9ndj0jekcch49kwr0gwy7upe3ffw | 148a525ec7bffedfc41cbc5339bf22d9e310d49b65831a269c86774fb732948c | | Lend Vault Registry (V2) | secret1qxk2scacpgj2mmm0af60674afl9e6qneg7yuny | ac5d501827d9a337a618ca493fcbf1323b20771378774a6bf466cb66361bf021 | | Lend Vault Registry (V3) | secret1wj2czeeknya2n6jag7kpfxlm28dw7q96dgqmfs | d837f716de3732a4118fbcb6d4cd0ef1d84ee83fef924f27b7c2a821f8528b39 | -| Lend Stability Pool | secret1wdxqz26acf2e6rsac8007pd53ak7n8tgeqr46w | 4dcdce6a2f88ef2912b9988119b345b096909aa4ba3881eff19358d983c40210 | -| Index Oracle (SILK) | secret1552yh3rplmyrjwhcxrq0egg35uy6zwjtszecf0 | 8d2b439383091ecb7806757a2b202e0056e542ade67951a0d5c352e74ce416cc | +| Lend Stability Pool | secret1wdxqz26acf2e6rsac8007pd53ak7n8tgeqr46w | 4dcdce6a2f88ef2912b9988119b345b096909aa4ba3881eff19358d983c40210 | +| Index Oracle (SILK) | secret1552yh3rplmyrjwhcxrq0egg35uy6zwjtszecf0 | 8d2b439383091ecb7806757a2b202e0056e542ade67951a0d5c352e74ce416cc | ::: tip The ShadeSwap pairs contracts are accessible via the factory registered pairs query.