From a52293662523be13a0c586e760e70a6935a2c67a Mon Sep 17 00:00:00 2001 From: Carlos Martinho Date: Tue, 10 Oct 2023 20:58:45 +0100 Subject: [PATCH] feat: make sharable package --- .eslintignore | 5 +- package.json | 3 +- src/client/index.js | 62 ++++++++++++++++++++++++ src/client/services/clientServices.js | 22 +++++++++ src/client/services/createFetch.js | 24 +++++++++ src/config.js | 5 ++ src/contracts/definitions/oracle.js | 17 +++++++ src/contracts/definitions/oracle.test.js | 18 +++++++ src/contracts/services/oracle.js | 44 +++++++++++++++++ src/contracts/services/oracle.test.js | 49 +++++++++++++++++++ src/errors/index.js | 15 ++++++ src/index.js | 2 + src/index.ts | 6 +++ src/lib/utils.js | 19 ++++++++ src/lib/utils.test.js | 21 ++++++++ src/types/client.js | 1 + src/types/contracts/oracle.js | 1 + src/types/wallet.js | 1 + vite.config.js | 25 ++++++++++ vite.config.ts | 11 +++++ 20 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 src/client/index.js create mode 100644 src/client/services/clientServices.js create mode 100644 src/client/services/createFetch.js create mode 100644 src/config.js create mode 100644 src/contracts/definitions/oracle.js create mode 100644 src/contracts/definitions/oracle.test.js create mode 100644 src/contracts/services/oracle.js create mode 100644 src/contracts/services/oracle.test.js create mode 100644 src/errors/index.js create mode 100644 src/index.js create mode 100644 src/index.ts create mode 100644 src/lib/utils.js create mode 100644 src/lib/utils.test.js create mode 100644 src/types/client.js create mode 100644 src/types/contracts/oracle.js create mode 100644 src/types/wallet.js create mode 100644 vite.config.js diff --git a/.eslintignore b/.eslintignore index 76add87..202fe05 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ -node_modules -dist \ No newline at end of file +**/node_modules/** +*.d.ts +*.js diff --git a/package.json b/package.json index 4823488..ac8f047 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "shadejs", - "version": "0.0.1", + "version": "0.0.2", "author": "Secure Secrets", "license": "MIT", "scripts": { + "build": "tsc && vite build", "commitizen": "cz", "lint": "eslint \"**/*.{ts,js}\"", "lint:fix": "eslint \"**/*.{vue,ts,js}\" --fix", diff --git a/src/client/index.js b/src/client/index.js new file mode 100644 index 0000000..741007a --- /dev/null +++ b/src/client/index.js @@ -0,0 +1,62 @@ +import { of, defer, tap, first, } from 'rxjs'; +import { SecretNetworkClient, } from 'secretjs'; +import { createFetchClient } from '~/client/services/createFetch'; +import { DEFAULT_SECRET_LCD_ENDPOINT, SECRET_MAINNET_CHAIN_ID, } from '~/config'; +/** + * Create and returns Secret Network client + * @param walletAccount not required for making public queries + */ +const getSecretNetworkClient$ = ({ walletAccount, lcdEndpoint, chainId, }) => createFetchClient(defer(() => { + if (walletAccount) { + return of({ + client: new SecretNetworkClient({ + url: lcdEndpoint, + wallet: walletAccount.signer, + walletAddress: walletAccount.walletAddress, + chainId, + encryptionUtils: walletAccount.encryptionUtils, + encryptionSeed: walletAccount.encryptionSeed, + }), + endpoint: lcdEndpoint, + chainId, + }); + } + return of({ + client: new SecretNetworkClient({ + url: lcdEndpoint, + chainId, + }), + endpoint: lcdEndpoint, + chainId, + }); +})); +let activeClient; +/** + * Gets the active query client. If one does not exist, initialize it and stores it for + * future use. + * @param lcdEndpoint uses a default mainnet endpoint if one is not provided + * @param chainId uses a default mainnet chainID if one is not provided + */ +function getActiveQueryClient$(lcdEndpoint, chainId) { + // check if a client exists and return it if it does + // as long as the endpoint and chain Id haven't changed from previous. + if (activeClient + && lcdEndpoint + && lcdEndpoint === activeClient.endpoint + && chainId + && chainId === activeClient.chainId) { + return of(activeClient); + } + // if no endpoint/chainId is provided, assume mainnet and use defaults + return getSecretNetworkClient$({ + lcdEndpoint: lcdEndpoint !== null && lcdEndpoint !== void 0 ? lcdEndpoint : DEFAULT_SECRET_LCD_ENDPOINT, + chainId: chainId !== null && chainId !== void 0 ? chainId : SECRET_MAINNET_CHAIN_ID, + }).pipe(tap(({ client }) => { + activeClient = { + client, + endpoint: lcdEndpoint !== null && lcdEndpoint !== void 0 ? lcdEndpoint : DEFAULT_SECRET_LCD_ENDPOINT, + chainId: chainId !== null && chainId !== void 0 ? chainId : SECRET_MAINNET_CHAIN_ID, + }; + }), first()); +} +export { getSecretNetworkClient$, getActiveQueryClient$, }; diff --git a/src/client/services/clientServices.js b/src/client/services/clientServices.js new file mode 100644 index 0000000..93317d0 --- /dev/null +++ b/src/client/services/clientServices.js @@ -0,0 +1,22 @@ +import { first, defer, from, tap, } from 'rxjs'; +import { createFetchClient } from '~/client/services/createFetch'; +import { identifyQueryResponseErrors } from '~/errors'; +/** + * query the contract using a secret client + */ +const secretClientContractQuery$ = ({ queryMsg, client, contractAddress, codeHash, }) => createFetchClient(defer(() => from(client.query.compute.queryContract({ + contract_address: contractAddress, + code_hash: codeHash, + query: queryMsg, +})))); +/** + * sets up the service observable for calling the querying with the secret client + */ +const sendSecretClientContractQuery$ = ({ queryMsg, client, contractAddress, codeHash, }) => secretClientContractQuery$({ + queryMsg, + client, + contractAddress, + codeHash, +}) + .pipe(tap((response) => identifyQueryResponseErrors(response)), first()); +export { sendSecretClientContractQuery$, }; diff --git a/src/client/services/createFetch.js b/src/client/services/createFetch.js new file mode 100644 index 0000000..cbf8ea0 --- /dev/null +++ b/src/client/services/createFetch.js @@ -0,0 +1,24 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +import { catchError, of, switchMap, first, } from 'rxjs'; +function createFetchClient(data$) { + return data$.pipe(switchMap((response) => of(response)), first(), catchError((err) => { + throw err; + })); +} +function createFetch(data$) { + return data$.pipe(switchMap((response) => __awaiter(this, void 0, void 0, function* () { + if (response.ok) { + return response.json(); + } + throw new Error('Fetch Error'); + }))); +} +export { createFetchClient, createFetch, }; diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..4d4781c --- /dev/null +++ b/src/config.js @@ -0,0 +1,5 @@ +// default endpoint to be used when alternative endpoint is not provided +// this is a public endpoint and not guarantees can be provided about the performance +const DEFAULT_SECRET_LCD_ENDPOINT = 'https://lcd.secret.express/'; +const SECRET_MAINNET_CHAIN_ID = 'secret-4'; +export { DEFAULT_SECRET_LCD_ENDPOINT, SECRET_MAINNET_CHAIN_ID, }; diff --git a/src/contracts/definitions/oracle.js b/src/contracts/definitions/oracle.js new file mode 100644 index 0000000..656d394 --- /dev/null +++ b/src/contracts/definitions/oracle.js @@ -0,0 +1,17 @@ +/** + * Query single price from the oracle contract + */ +const msgQueryOraclePrice = (oracleKey) => ({ + get_price: { + key: oracleKey, + }, +}); +/** + * Query muliple prices from the oracle contract + */ +const msgQueryOraclePrices = (oracleKeys) => ({ + get_prices: { + keys: oracleKeys, + }, +}); +export { msgQueryOraclePrice, msgQueryOraclePrices, }; diff --git a/src/contracts/definitions/oracle.test.js b/src/contracts/definitions/oracle.test.js new file mode 100644 index 0000000..116d3b9 --- /dev/null +++ b/src/contracts/definitions/oracle.test.js @@ -0,0 +1,18 @@ +import { test, expect, } from 'vitest'; +import { msgQueryOraclePrice, msgQueryOraclePrices, } from '~/contracts/definitions/oracle'; +test('it test the form of the query oracle msg', () => { + const output = { + get_price: { + key: 'MOCK_KEY', + }, + }; + expect(msgQueryOraclePrice('MOCK_KEY')).toStrictEqual(output); +}); +test('it test the form of the query oracle msg', () => { + const output = { + get_prices: { + keys: ['MOCK_KEY_1', 'MOCK_KEY_2'], + }, + }; + expect(msgQueryOraclePrices(['MOCK_KEY_1', 'MOCK_KEY_2'])).toStrictEqual(output); +}); diff --git a/src/contracts/services/oracle.js b/src/contracts/services/oracle.js new file mode 100644 index 0000000..fc6f48e --- /dev/null +++ b/src/contracts/services/oracle.js @@ -0,0 +1,44 @@ +import { switchMap, first, map, } from 'rxjs'; +import { sendSecretClientContractQuery$ } from '~/client/services/clientServices'; +import { getActiveQueryClient$ } from '~/client'; +import { convertCoinFromUDenom } from '~/lib/utils'; +import { msgQueryOraclePrice, msgQueryOraclePrices } from '~/contracts/definitions/oracle'; +/** +* Parses the contract price query into the app data model +*/ +const parsePriceFromContract = (response) => ({ + oracleKey: response.key, + rate: convertCoinFromUDenom(response.data.rate, 18), + lastUpdatedBase: response.data.last_updated_base, + lastUpdatedQuote: response.data.last_updated_quote, +}); +/** +* Parses the contract prices query into the app data model +*/ +function parsePricesFromContract(pricesResponse) { + return pricesResponse.reduce((prev, curr) => (Object.assign(Object.assign({}, prev), { [curr.key]: { + oracleKey: curr.key, + rate: convertCoinFromUDenom(curr.data.rate, 18), + lastUpdatedBase: curr.data.last_updated_base, + lastUpdatedQuote: curr.data.last_updated_quote, + } })), {}); +} +/** + * query the price of an asset using the oracle key + */ +const queryPrice$ = ({ contractAddress, codeHash, oracleKey, lcdEndpoint, chainId, }) => getActiveQueryClient$(lcdEndpoint, chainId).pipe(switchMap(({ client }) => sendSecretClientContractQuery$({ + queryMsg: msgQueryOraclePrice(oracleKey), + client, + contractAddress, + codeHash, +})), map((response) => parsePriceFromContract(response)), first()); +/** + * query multiple asset prices using oracle keys + */ +const queryPrices$ = ({ contractAddress, codeHash, oracleKeys, lcdEndpoint, chainId, }) => getActiveQueryClient$(lcdEndpoint, chainId).pipe(switchMap(({ client }) => sendSecretClientContractQuery$({ + queryMsg: msgQueryOraclePrices(oracleKeys), + client, + contractAddress, + codeHash, +})), map((response) => parsePricesFromContract(response)), first()); +export { parsePriceFromContract, parsePricesFromContract, queryPrice$, queryPrices$, }; diff --git a/src/contracts/services/oracle.test.js b/src/contracts/services/oracle.test.js new file mode 100644 index 0000000..398ac95 --- /dev/null +++ b/src/contracts/services/oracle.test.js @@ -0,0 +1,49 @@ +import { test, expect, vi, beforeAll, afterAll, } from 'vitest'; +import { parsePriceFromContract, parsePricesFromContract, } from '~/contracts/services/oracle'; +import priceResponse from '~/test/mocks/oracle/priceResponse.json'; +import pricesResponse from '~/test/mocks/oracle/pricesResponse.json'; +import BigNumber from 'bignumber.js'; +import { of } from 'rxjs'; +beforeAll(() => { + vi.mock('~/contracts/definitions/oracle', () => ({ + msgQueryOraclePrice: vi.fn(() => 'MSG_QUERY_ORACLE_PRICE'), + msgQueryOraclePrices: vi.fn(() => 'MSG_QUERY_ORACLE_PRICES'), + })); + vi.mock('~/client/index', () => ({ + getActiveQueryClient$: vi.fn(() => of('CLIENT')), + })); + vi.mock('~/client/services/clientServices', () => ({ + sendSecretClientContractQuery$: vi.fn(() => of()), + })); + vi.mock('~/client/services/clientServices', () => ({ + sendSecretClientContractQuery$: vi.fn(() => of()), + })); +}); +afterAll(() => { + vi.clearAllMocks(); +}); +test('it can parse the price response', () => { + expect(parsePriceFromContract(priceResponse)).toStrictEqual({ + oracleKey: 'BTC', + rate: BigNumber('27917.2071556'), + lastUpdatedBase: 1696644063, + lastUpdatedQuote: 18446744073709552000, + }); +}); +test('it can parse the prices response', () => { + const parsedOutput = { + BTC: { + oracleKey: 'BTC', + rate: BigNumber('27917.2071556'), + lastUpdatedBase: 1696644063, + lastUpdatedQuote: 18446744073709552000, + }, + ETH: { + oracleKey: 'ETH', + rate: BigNumber('1644.0836829'), + lastUpdatedBase: 1696644063, + lastUpdatedQuote: 18446744073709552000, + }, + }; + expect(parsePricesFromContract(pricesResponse)).toStrictEqual(parsedOutput); +}); diff --git a/src/errors/index.js b/src/errors/index.js new file mode 100644 index 0000000..0354885 --- /dev/null +++ b/src/errors/index.js @@ -0,0 +1,15 @@ +/** + * Parse contract query responses to determine if an error has occured + */ +function identifyQueryResponseErrors(response) { + if (typeof response === 'string' + && (response.includes('error') || response.includes('Error'))) { + throw new Error(response); + } + if (typeof response === 'object' + && 'includes' in response + && response.includes('parse_err')) { + throw new Error(response); + } +} +export { identifyQueryResponseErrors, }; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..d4f6ac7 --- /dev/null +++ b/src/index.js @@ -0,0 +1,2 @@ +import { getSecretNetworkClient$, getActiveQueryClient$ } from '~/client'; +export { getSecretNetworkClient$, getActiveQueryClient$, }; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..22fb723 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,6 @@ +import { getSecretNetworkClient$, getActiveQueryClient$ } from '~/client'; + +export { + getSecretNetworkClient$, + getActiveQueryClient$, +}; diff --git a/src/lib/utils.js b/src/lib/utils.js new file mode 100644 index 0000000..5ccc155 --- /dev/null +++ b/src/lib/utils.js @@ -0,0 +1,19 @@ +import BigNumber from 'bignumber.js'; +/** + * Convert from uDenom to the human readable equivalent as BigNumber type + */ +const convertCoinFromUDenom = (amount, decimals) => { + BigNumber.config({ DECIMAL_PLACES: 18 }); + return BigNumber(amount).dividedBy(BigNumber(10).pow(decimals)); +}; +/** + * Convert BigNumber to the uDenom string type + */ +const convertCoinToUDenom = (amount, decimals) => { + BigNumber.config({ DECIMAL_PLACES: 18 }); + if (typeof amount === 'string' || typeof amount === 'number') { + return BigNumber(amount).multipliedBy(BigNumber(10).pow(decimals)).toFixed(0); + } + return amount.multipliedBy(BigNumber(10).pow(decimals)).toFixed(0); +}; +export { convertCoinToUDenom, convertCoinFromUDenom, }; diff --git a/src/lib/utils.test.js b/src/lib/utils.test.js new file mode 100644 index 0000000..abbb0f5 --- /dev/null +++ b/src/lib/utils.test.js @@ -0,0 +1,21 @@ +import { test, expect, } from 'vitest'; +import { BigNumber } from 'bignumber.js'; +import { convertCoinFromUDenom, convertCoinToUDenom, } from './utils'; +test('It converts token from U denom V2', () => { + expect(convertCoinFromUDenom(1000, 2)).toStrictEqual(BigNumber(10)); + expect(convertCoinFromUDenom('1000', 2)).toStrictEqual(BigNumber(10)); + expect(convertCoinFromUDenom('1000000000000000000000000000', 2)).toStrictEqual(BigNumber('10000000000000000000000000')); + expect(convertCoinFromUDenom(1e100, 2)).toStrictEqual(BigNumber(1e98)); + expect(convertCoinFromUDenom('1e100', 2)).toStrictEqual(BigNumber(1e98)); + expect(convertCoinFromUDenom('100000000000000000000005555.123456789123456789', 2)).toStrictEqual(BigNumber('1000000000000000000000055.551234567891234568')); + expect(convertCoinFromUDenom('987654321987654321987654321', 18)).toStrictEqual(BigNumber('987654321.987654321987654321')); +}); +test('It converts token to U denom V2', () => { + const testBigNumber1 = BigNumber(1000); + expect(convertCoinToUDenom(testBigNumber1, 2)).toBe('100000'); + const testBigNumber2 = BigNumber('0.123456789123456789'); + expect(convertCoinToUDenom(testBigNumber2, 18)).toBe('123456789123456789'); + const testBigNumber3 = BigNumber('123456789123456789.123456789123456789'); + expect(convertCoinToUDenom(testBigNumber3, 18)).toBe('123456789123456789123456789123456789'); + expect(convertCoinToUDenom('1111.123456789101213141', 18)).toBe('1111123456789101213141'); +}); diff --git a/src/types/client.js b/src/types/client.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/types/client.js @@ -0,0 +1 @@ +export {}; diff --git a/src/types/contracts/oracle.js b/src/types/contracts/oracle.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/types/contracts/oracle.js @@ -0,0 +1 @@ +export {}; diff --git a/src/types/wallet.js b/src/types/wallet.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/types/wallet.js @@ -0,0 +1 @@ +export {}; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..803cd24 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite'; +import path from 'path'; +// https://vitejs.dev/config/ +export default defineConfig({ + // @ts-ignore + // vitest configs + test: { + globals: true, + }, + resolve: { + alias: { + '~': `${path.resolve(__dirname, 'src')}`, + }, + }, + build: { + manifest: true, + minify: true, + reportCompressedSize: true, + lib: { + entry: path.resolve(__dirname, 'src/index.ts'), + fileName: 'main', + formats: ['es'], + }, + }, +}); diff --git a/vite.config.ts b/vite.config.ts index c01bac2..6f21f5b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,4 +15,15 @@ export default defineConfig({ '~': `${path.resolve(__dirname, 'src')}`, }, }, + + build: { + manifest: true, + minify: true, + reportCompressedSize: true, + lib: { + entry: path.resolve(__dirname, 'src/index.ts'), + fileName: 'main', + formats: ['es'], + }, + }, });