diff --git a/README.md b/README.md index 692e2dce4c..59ae0396c8 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,23 @@ yarn build yarn test ``` +# 🔎 Explorer + +Start the Explorer on http://localhost:4200 + +[Read more about the Explorer](apps/explorer/README.md) + +### Start +```bash +yarn start:explorer +``` + +### Build +```bash +yarn build:explorer +``` + + # 🖼️ Widget Configurator Start the Widget Configurator on http://127.0.0.1:4200/widget-configurator diff --git a/apps/explorer/.env.example b/apps/explorer/.env.example new file mode 100644 index 0000000000..e3bb230873 --- /dev/null +++ b/apps/explorer/.env.example @@ -0,0 +1,16 @@ +# Enables mock mode (default = true) +MOCK=true + +# Enables autoconnect for mock mode (default = true) +AUTOCONNECT=true + +# Public IPFS gateway +REACT_APP_IPFS_READ_URI=https://cloudflare-ipfs.com/ipfs + +# Sentry +#REACT_APP_SENTRY_DSN='https://' +#REACT_APP_SENTRY_TRACES_SAMPLE_RATE="1.0" +#REACT_APP_SENTRY_AUTH_TOKEN='' + +# Orderbook API Endpoints +#REACT_APP_ORDER_BOOK_URLS='{"1":"https://YOUR_HOST","100":"https://YOUR_HOST","5":"https://YOUR_HOST"} diff --git a/apps/explorer/README.md b/apps/explorer/README.md new file mode 100644 index 0000000000..6b495f48d2 --- /dev/null +++ b/apps/explorer/README.md @@ -0,0 +1,35 @@ +# Explorer + +## 🏃‍♀️ Run it locally + +```bash +yarn start:explorer +``` + + +## 👷‍♀️ Build app + +```bash +yarn build:explorer +``` + +Static files will be generated inside the `build/explorer` dir. + +## 🧪 Run tests + +```bash +yarn test:explorer +``` + +## Orderbook API Endpoints + +Fee quote requests and posting orders are sent to the Orderbook API. This API has the responsibility of collecting orders and +handing them to the solvers. + +The reference implementation of the API is [CoW Protocol Services](https://github.com/cowprotocol/services). + +The API endpoint is configured using the environment variable `REACT_APP_ORDER_BOOK_URLS`: + +```ini +REACT_APP_ORDER_BOOK_URLS={"1":"https://YOUR_HOST","100":"https://YOUR_HOST","5":"https://YOUR_HOST"} +``` diff --git a/apps/explorer/src/api/operator/index.ts b/apps/explorer/src/api/operator/index.ts index 02512a929b..b672326403 100644 --- a/apps/explorer/src/api/operator/index.ts +++ b/apps/explorer/src/api/operator/index.ts @@ -11,7 +11,6 @@ const useMock = process.env.MOCK_OPERATOR === 'true' export const { // functions that have a mock getOrder, - getOrders, getAccountOrders, getTxOrders, getTrades, diff --git a/apps/explorer/src/api/operator/operatorApi.ts b/apps/explorer/src/api/operator/operatorApi.ts index c7b17d00bf..b775ac77b3 100644 --- a/apps/explorer/src/api/operator/operatorApi.ts +++ b/apps/explorer/src/api/operator/operatorApi.ts @@ -1,62 +1,9 @@ -import { Network } from 'types' -import { buildSearchString } from 'utils/url' -import { isProd, isStaging } from 'utils/env' - -import { GetOrderParams, GetOrdersParams, RawOrder, RawTrade, GetTxOrdersParams, WithNetworkId } from './types' -import { fetchQuery } from 'api/baseApi' +import { GetOrderParams, RawOrder, RawTrade, GetTxOrdersParams, WithNetworkId } from './types' import { orderBookSDK } from 'cowSdk' import { Address, UID } from '@cowprotocol/cow-sdk' export { getAccountOrders } from './accountOrderUtils' -// TODO: export this through the sdk -export type ApiEnv = 'prod' | 'staging' - -// TODO: should come from the SDK by now, find out where this is used -function getOperatorUrl(): Partial> { - if (isProd || isStaging) { - return { - [Network.MAINNET]: process.env.OPERATOR_URL_PROD_MAINNET, - [Network.GOERLI]: process.env.OPERATOR_URL_PROD_GOERLI, - [Network.GNOSIS_CHAIN]: process.env.OPERATOR_URL_PROD_XDAI, - } - } else { - return { - [Network.MAINNET]: process.env.OPERATOR_URL_STAGING_MAINNET, - [Network.GOERLI]: process.env.OPERATOR_URL_STAGING_GOERLI, - [Network.GNOSIS_CHAIN]: process.env.OPERATOR_URL_STAGING_XDAI, - } - } -} - -const API_BASE_URL = getOperatorUrl() - -const DEFAULT_HEADERS: Headers = new Headers({ - 'Content-Type': 'application/json', -}) - -/** - * Unique identifier for the order, calculated by keccak256(orderDigest, ownerAddress, validTo), - where orderDigest = keccak256(orderStruct). bytes32. - */ - -function _getApiBaseUrl(networkId: Network): string { - const baseUrl = API_BASE_URL[networkId] - - if (!baseUrl) { - throw new Error('Unsupported Network. The operator API is not deployed in the Network ' + networkId) - } else { - return baseUrl + '/v1' - } -} - -function _get(networkId: Network, url: string): Promise { - const baseUrl = _getApiBaseUrl(networkId) - return fetch(baseUrl + url, { - headers: DEFAULT_HEADERS, - }) -} - /** * Gets a single order by id */ @@ -66,37 +13,6 @@ export async function getOrder(params: GetOrderParams): Promise return orderBookSDK.getOrderMultiEnv(orderId, { chainId: networkId }) } -/** - * Gets a list of orders - * - * Optional filters: - * - owner: address - * - sellToken: address - * - buyToken: address - * - minValidTo: number - */ -export async function getOrders(params: GetOrdersParams): Promise { - const { networkId, ...searchParams } = params - const { owner, sellToken, buyToken, minValidTo } = searchParams - const defaultValues = { - includeFullyExecuted: 'true', - includeInvalidated: 'true', - includeInsufficientBalance: 'true', - includePresignaturePending: 'true', - includeUnsupportedTokens: 'true', - } - - console.log( - `[getOrders] Fetching orders on network ${networkId} with filters: owner=${owner} sellToken=${sellToken} buyToken=${buyToken}` - ) - - const searchString = buildSearchString({ ...searchParams, ...defaultValues, minValidTo: String(minValidTo) }) - - const queryString = '/orders/' + searchString - - return _fetchQuery(networkId, queryString) -} - /** * Gets a order list within Tx */ @@ -148,14 +64,3 @@ export async function getTrades( return [...trades[0], ...trades[1]] } - -function _fetchQuery(networkId: Network, queryString: string): Promise -function _fetchQuery(networkId: Network, queryString: string, nullOn404: true): Promise -function _fetchQuery(networkId: Network, queryString: string, nullOn404?: boolean): Promise { - const get = (): Promise => _get(networkId, queryString) - if (nullOn404) { - return fetchQuery({ get }, queryString, nullOn404) - } - - return fetchQuery({ get }, queryString) -} diff --git a/apps/explorer/src/cowSdk.ts b/apps/explorer/src/cowSdk.ts index 4dfe93a547..3d7df7f1f8 100644 --- a/apps/explorer/src/cowSdk.ts +++ b/apps/explorer/src/cowSdk.ts @@ -2,7 +2,14 @@ import { OrderBookApi, SubgraphApi } from '@cowprotocol/cow-sdk' import { MetadataApi } from '@cowprotocol/app-data' import { SUBGRAPH_URLS } from './consts/subgraphUrls' -export const orderBookSDK = new OrderBookApi({ env: 'prod' }) +const prodBaseUrls = process.env.REACT_APP_ORDER_BOOK_URLS + ? JSON.parse(process.env.REACT_APP_ORDER_BOOK_URLS) + : undefined + +export const orderBookSDK = new OrderBookApi({ + env: 'prod', + ...(prodBaseUrls ? { baseUrls: prodBaseUrls } : undefined), +}) export const subgraphApiSDK = new SubgraphApi({ baseUrls: SUBGRAPH_URLS, }) diff --git a/apps/explorer/src/explorer/pages/AppData/EncodePage.tsx b/apps/explorer/src/explorer/pages/AppData/EncodePage.tsx index a066078034..08196d184b 100644 --- a/apps/explorer/src/explorer/pages/AppData/EncodePage.tsx +++ b/apps/explorer/src/explorer/pages/AppData/EncodePage.tsx @@ -103,7 +103,7 @@ const EncodePage: React.FC = ({ tabData, setTabData /* handleTabCha useEffect(() => { setIsLoading(true) - // Get the fullAppData (dermenistic stringify JSON) + // Get the fullAppData (deterministic stringify JSON) _toFullAppData(appDataForm) .then((fullAppData) => { // Update the fullAppData @@ -133,11 +133,6 @@ const EncodePage: React.FC = ({ tabData, setTabData /* handleTabCha [] ) - // const handleIPFSErrors = useCallback( - // (_: FormProps, errors: FormValidation): FormValidation => handleErrors(ipfsFormRef, errors, setDisabledIPFS), - // [], - // ) - const handleOnChange = useCallback( ({ formData }: FormProps): void => { const resetFormFields = (form: string): void => { @@ -157,34 +152,6 @@ const EncodePage: React.FC = ({ tabData, setTabData /* handleTabCha [ipfsHashInfo] ) - // const handleIPFSOnChange = useCallback(({ formData: ipfsData }: FormProps): void => { - // setIpfsCredentials(ipfsData) - // if (JSON.stringify(ipfsData) !== JSON.stringify({})) { - // setDisabledIPFS(false) - // } - // }, []) - - // const onUploadToIPFS = useCallback( - // async ({ formData }: FormProps): Promise => { - // if (!ipfsHashInfo) return - // setIsLoading(true) - // try { - // await metadataApiSDK.uploadMetadataDocToIpfsLegacy(handleFormatData(appDataForm), formData) - // setIsDocUploaded(true) - // } catch (e) { - // if (INVALID_IPFS_CREDENTIALS.includes(e.message)) { - // e.message = 'Invalid API keys provided.' - // } - // setError(e.message) - // setIsDocUploaded(false) - // } finally { - // setIsLoading(false) - // toggleInvalid({ ipfs: true }) - // } - // }, - // [appDataForm, ipfsHashInfo], - // ) - return ( <>
@@ -319,91 +286,6 @@ const EncodePage: React.FC = ({ tabData, setTabData /* handleTabCha
- {/*
- {ipfsHashInfo && ( - <> - -
-

IPFS UPLOAD

-

- We offer an integrated way to upload the generated file to IPFS directly through - - Pinata’s - - API. -

-

- Insert your credentials and hit upload. The resulting appData hash is displayed on - the generated section. -

-

- Once uploaded, you can use the appData hash in the - handleTabChange(TabView.DECODE)}>Decode - tab to verify it. -

-
-
toggleInvalid({ ipfs: true })} - transformErrors={transformErrors} - schema={ipfsSchema} - uiSchema={ipfsUiSchema} - > - - Notes: -
    -
  1. - The credentials are kept in memory only while you are at this page. When you navigate away or - close the browser tab it’ll be cleared. -
  2. -
  3. - If you upload the file directly, the resulting hash/appDataHash might differ. The hash/IPFS CID - calculated by the tool is a minified and stringified file without a new line at the end. That - means that you will get different results if the file is uploaded directly as a file. -
  4. -
-
- -
-
- - )} - {isDocUploaded && ( - <> - - {ipfsHashInfo?.cid} - - } - /> - - - )} - {isLoading && } - {error && !isDocUploaded && ( - - )} -
- */} ) } diff --git a/apps/explorer/src/explorer/pages/AppData/config.tsx b/apps/explorer/src/explorer/pages/AppData/config.tsx index 90b7125143..fb9e776fe1 100644 --- a/apps/explorer/src/explorer/pages/AppData/config.tsx +++ b/apps/explorer/src/explorer/pages/AppData/config.tsx @@ -63,23 +63,6 @@ export const handleErrors = ( return errors } -// export const ipfsSchema: JSONSchema7 = { -// type: 'object', -// required: ['pinataApiKey', 'pinataApiSecret'], -// properties: { -// pinataApiKey: { -// type: 'string', -// title: 'Pinata API key', -// description: 'Add your Pinata API key.', -// }, -// pinataApiSecret: { -// type: 'string', -// title: 'Pinata API secret', -// description: 'Add your Pinata API secret.', -// }, -// }, -// } - export const decodeAppDataSchema: JSONSchema7 = { type: 'object', title: 'AppData Decode', @@ -149,14 +132,3 @@ export const uiSchema = { }, }, } - -// export const ipfsUiSchema = { -// pinataApiKey: { -// 'ui:field': 'cField', -// tooltip: 'Add your Pinata API key.', -// }, -// pinataApiSecret: { -// 'ui:field': 'cField', -// tooltip: 'Add your Pinata API secret key.', -// }, -// } diff --git a/apps/explorer/vite.config.ts b/apps/explorer/vite.config.ts index fffdcff5e1..14b7d40e68 100644 --- a/apps/explorer/vite.config.ts +++ b/apps/explorer/vite.config.ts @@ -9,67 +9,75 @@ import dynamicImport from 'vite-plugin-dynamic-import' import { version as APP_VERSION } from './package.json' import { version as CONTRACT_VERSION } from '@cowprotocol/contracts/package.json' import { version as DEX_JS_VERSION } from '@gnosis.pm/dex-js/package.json' +import { getReactProcessEnv } from '../../tools/getReactProcessEnv' const CONFIG = loadConfig() -export default defineConfig({ - base: './', - cacheDir: '../../node_modules/.vite/explorer', +export default defineConfig(({ mode }) => { + return { + base: './', + cacheDir: '../../node_modules/.vite/explorer', - define: { - CONFIG, - VERSION: `'${APP_VERSION}'`, - CONTRACT_VERSION: `'${CONTRACT_VERSION}'`, - DEX_JS_VERSION: `'${DEX_JS_VERSION}'`, - }, - - server: { - port: 4200, - host: 'localhost', - fs: { - allow: [ - // search up for workspace root - searchForWorkspaceRoot(process.cwd()), - // your custom rules - 'apps/explorer/src', - 'libs', - ], + resolve: { + extensions: mode === 'development' ? ['.js', '.ts', '.jsx', '.tsx', '.json'] : undefined, }, - }, - preview: { - port: 4300, - host: 'localhost', - }, + define: { + ...getReactProcessEnv(mode), + CONFIG, + VERSION: `'${APP_VERSION}'`, + CONTRACT_VERSION: `'${CONTRACT_VERSION}'`, + DEX_JS_VERSION: `'${DEX_JS_VERSION}'`, + }, - plugins: [ - nodePolyfills({ - globals: { - Buffer: true, - global: true, - process: true, + server: { + port: 4200, + host: 'localhost', + fs: { + allow: [ + // search up for workspace root + searchForWorkspaceRoot(process.cwd()), + // your custom rules + 'apps/explorer/src', + 'libs', + ], }, - protocolImports: true, - }), - react({}), - viteTsConfigPaths({ - root: '../../', - }), - dynamicImport({ - filter(id) { - if (id.includes('/node_modules/@cowprotocol')) { - return true - } - }, - }), - ], + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [ + nodePolyfills({ + globals: { + Buffer: true, + global: true, + process: true, + }, + protocolImports: true, + }), + react({}), + viteTsConfigPaths({ + root: '../../', + }), + dynamicImport({ + filter(id) { + if (id.includes('/node_modules/@cowprotocol')) { + return true + } + }, + }), + ], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ - // viteTsConfigPaths({ - // root: '../../', - // }), - // ], - // }, + // Uncomment this if you are using workers. + // worker: { + // plugins: [ + // viteTsConfigPaths({ + // root: '../../', + // }), + // ], + // }, + } })