From cb3aa89463e36be53d6253723fe064b3fd8b767b Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Tue, 7 May 2024 14:20:26 -0400 Subject: [PATCH] Publish Stage (#3198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove log * Add automated e2e tests workflow (#3104) * Add and upload junit and html reports * Add .github/workflows/frontend-e2e-tests.yml * added env with private key * run just 2 tests with 0 retry * timeout to 60 and better connectWallet * run only on master/stage * Split E2E workflow per stage/master branch (#3197) * Split E2E workflow per stage/master branch * upload just html report for stage * Add transactions link (#3194) * Mattupham/fe 346 fix table overflow behavior on mobile (#3189) * Cleanup * clean up rows * add padding * Style updaetes * Clean up transaction columns * Add comment * Fix simulate on edge runtime (#3195) uses the node Request/Response API * optional logoURIs (#3200) * log top gainers category (#3202) * Run E2E tests on Production and Preview deployments (#3201) * Run tests based on environment name * Swap Amplitude event: use valid number in swap event (#3204) * use valid number in swap event * log * Update ibc-overrides.ts (#3203) * Update rows, swap numbers, update sidebar, add notes (#3199) * Update rows, swap numbers, update sidebar, add notes * Clean up mobile * Fix logoURIs issue * Logo URIs optional * Cleanup * Add dollar sign * Update onload * Clean up * Add font classes * add currency symbol * Flip details slideover * Add en translations * Add translations= * Clean up utils * update text * Translations * Add check for amount and usd * Clean up * Clean up * Add note * use anchor for balances moved banner (#3192) * QA updates - i18n, pools table header fixes (#3205) * Updates * Clean up classes * Table header fixes * Fix asset list build: get chain name from first counterparty (IBC) (#3207) * get chain name from first counterparty (IBC) * check for native * fix asset list * throw clearer error * Added USDT and USDT.axl tests (#3206) * Added USDT and USDT.axl tests * Added information about workflow to the readme * skip Sidecar — ASTRO <> OSMO test * fix: :bug: fix circulating supply formatting (#3210) * feat: add token chart queries by denom (#3211) * feat: :sparkles: add token chart queries by denom * chore: :memo: update comment format * Add 1h, 7d price change to assets table (#3212) * chore: remove repetitive words (#3208) Signed-off-by: ianlv * Fix layout (#3213) * fix z index * fix nav bar * fix z index for mobile nav menu (#3216) * Assets page: update amplitude events (#3217) * rename file for easier discoverability * add highlights amplitude events * 24h * logs * Assets page: implement watchlist (#3214) * rename to watchlist * update turbo * remove custom sort function * add default sort * unify wach list icon in old table * fix pools show more button * style (#3218) * fix: :bug: fix twitter search (#3224) * fix button disabled (#3222) --------- Signed-off-by: ianlv Co-authored-by: yakuramori <62520712+yury-dubinin@users.noreply.github.com> Co-authored-by: Matt Upham <30577966+mattupham@users.noreply.github.com> Co-authored-by: JeremyParish69 <95667791+JeremyParish69@users.noreply.github.com> Co-authored-by: Davide Segullo Co-authored-by: ianlv <168640168+ianlv@users.noreply.github.com> --- .github/workflows/frontend-e2e-tests.yml | 85 ++++++++++ package.json | 2 +- packages/math/src/pool/concentrated/math.ts | 4 +- packages/server/src/queries/complex/README.md | 2 +- .../src/queries/complex/assets/market.ts | 12 ++ .../complex/assets/price/historical.ts | 6 +- .../complex/transactions/transactions.ts | 123 ++++++++------ .../src/queries/data-services/token-data.ts | 2 + .../data-services/token-historical-chart.ts | 10 +- .../server/src/queries/twitter/twitter.ts | 51 +++--- .../__tests_e2e__/swap-router.test.ts | 2 +- .../server/src/trpc-routers/assets-router.ts | 55 +++++-- .../server/src/utils/__tests__/sort.spec.ts | 26 --- packages/server/src/utils/index.ts | 1 + packages/server/src/utils/sort.ts | 7 +- .../swap-exact-in-positions.spec.ts | 2 +- packages/types/src/asset-types.ts | 6 +- packages/web/README.md | 14 +- packages/web/components/assets/categories.tsx | 2 +- .../assets/highlights-categories.tsx | 45 +++++- .../components/cards/validator-squad-card.tsx | 2 +- .../components/complex/all-pools-table.tsx | 2 +- .../web/components/complex/assets-page-v1.tsx | 44 +++-- .../web/components/funnels/balances-moved.tsx | 11 +- packages/web/components/swap-tool/index.tsx | 9 +- packages/web/components/table/asset-info.tsx | 98 +++++++++--- .../web/components/table/assets-table-v1.tsx | 5 +- .../web/components/table/cells/asset-name.tsx | 31 ++-- packages/web/components/table/cells/asset.tsx | 29 +++- .../token-details/token-details.tsx | 1 + .../transactions/no-transactions-splash.tsx | 2 +- .../transactions/transaction-content.tsx | 114 +++++++------ .../transactions/transaction-details.tsx | 56 +++---- .../transactions/transaction-row.tsx | 150 +++++++++--------- .../transactions/transaction-utils.tsx | 27 ++++ packages/web/components/ui/button.tsx | 2 +- packages/web/components/ui/pagination.tsx | 50 +++--- ...er-analytics-v2.ts => analytics-events.ts} | 1 + packages/web/config/generate-lists.ts | 4 +- packages/web/config/ibc-overrides.ts | 6 +- packages/web/config/index.ts | 2 +- packages/web/e2e/pages/swap-page.ts | 20 ++- packages/web/e2e/tests/swap.wallet.spec.ts | 54 +++++-- .../hooks/ui-config/use-asset-info-config.ts | 4 +- packages/web/hooks/user-settings/index.ts | 2 +- .../use-user-favorite-asset-denoms.ts | 45 ------ .../hooks/user-settings/use-user-watchlist.ts | 45 ++++++ packages/web/localizations/de.json | 11 +- packages/web/localizations/en.json | 11 +- packages/web/localizations/es.json | 11 +- packages/web/localizations/fa.json | 11 +- packages/web/localizations/fr.json | 11 +- packages/web/localizations/gu.json | 11 +- packages/web/localizations/hi.json | 11 +- packages/web/localizations/ja.json | 11 +- packages/web/localizations/ko.json | 11 +- packages/web/localizations/pl.json | 11 +- packages/web/localizations/pt-br.json | 11 +- packages/web/localizations/ro.json | 11 +- packages/web/localizations/ru.json | 11 +- packages/web/localizations/tr.json | 11 +- packages/web/localizations/zh-cn.json | 11 +- packages/web/localizations/zh-hk.json | 11 +- packages/web/localizations/zh-tw.json | 11 +- packages/web/package.json | 2 +- .../web/pages/api/simulate-transaction.ts | 50 +++--- packages/web/pages/assets/[denom].tsx | 24 +-- packages/web/pages/stake.tsx | 3 +- packages/web/pages/transactions.tsx | 4 +- packages/web/playwright.config.ts | 12 +- packages/web/stores/assets/assets-store.ts | 20 ++- yarn.lock | 108 ++++++------- 72 files changed, 1111 insertions(+), 572 deletions(-) create mode 100644 .github/workflows/frontend-e2e-tests.yml rename packages/web/config/{user-analytics-v2.ts => analytics-events.ts} (99%) delete mode 100644 packages/web/hooks/user-settings/use-user-favorite-asset-denoms.ts create mode 100644 packages/web/hooks/user-settings/use-user-watchlist.ts diff --git a/.github/workflows/frontend-e2e-tests.yml b/.github/workflows/frontend-e2e-tests.yml new file mode 100644 index 00000000000..e47685c642d --- /dev/null +++ b/.github/workflows/frontend-e2e-tests.yml @@ -0,0 +1,85 @@ +name: Frontend E2E tests Workflow + +on: + deployment_status: + +jobs: + master-e2e-tests: + if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success' && github.event.deployment_status.environment == 'Production – osmosis-frontend' + runs-on: macos-latest + environment: + name: prod_swap_test + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: "**/node_modules" + key: ${{ runner.OS }}-20.x-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.OS }}-20.x- + - name: Install Playwright + run: | + yarn --cwd packages/web install --frozen-lockfile && npx playwright install --with-deps chromium + - name: Run Select Swap Pair tests on Master + env: + BASE_URL: "https://app.osmosis.zone" + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + run: | + cd packages/web + npx playwright test -g "Test Swap feature" + - name: upload test results + if: always() + id: e2e-test-results + uses: actions/upload-artifact@v4 + with: + name: main-e2e-test-results + path: packages/web/playwright-report + - name: upload junit test results + id: e2e-junit-results + uses: actions/upload-artifact@v4 + with: + name: main-e2e-junit-results + path: packages/web/test-results/test-results.xml + + preview-e2e-tests: + if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success' && github.event.deployment_status.environment == 'Preview – osmosis-frontend' + runs-on: macos-latest + environment: + name: prod_swap_test + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: "**/node_modules" + key: ${{ runner.OS }}-20.x-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.OS }}-20.x- + - name: Install Playwright + run: | + yarn --cwd packages/web install --frozen-lockfile && npx playwright install --with-deps chromium + - name: Run Select Swap Pair tests on Stage + env: + BASE_URL: ${{ github.event.deployment_status.environment_url }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + run: | + cd packages/web + npx playwright test -g "Test Swap feature" + - name: upload test results + if: always() + id: e2e-test-results + uses: actions/upload-artifact@v4 + with: + name: preview-e2e-test-results + path: packages/web/playwright-report diff --git a/package.json b/package.json index 480ec52d915..af88ad4647f 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "pre-commit": "^1.2.2", "prettier": "^2.8.8", "ts-jest": "^29.1.2", - "turbo": "^1.13.0", + "turbo": "^1.13.3", "typescript": "5.4.3" }, "packageManager": "yarn@1.22.22", diff --git a/packages/math/src/pool/concentrated/math.ts b/packages/math/src/pool/concentrated/math.ts index 7ea2fc92d4b..d8d6f268308 100644 --- a/packages/math/src/pool/concentrated/math.ts +++ b/packages/math/src/pool/concentrated/math.ts @@ -50,7 +50,7 @@ export function calcAmount0Delta( // the case where we want to round up to favor the pool. // Examples include: // - calculating amountIn during swap - // - adding liquidity (request user to provide more tokens in in favor of the pool) + // - adding liquidity (request user to provide more tokens in favor of the pool) // The denominator is truncated to get a higher final amount. const denom = sqrtPriceA.mulTruncate(sqrtPriceB); return liquidity.mul(diff).quo(denom).roundUpDec(); @@ -95,7 +95,7 @@ export function calcAmount1Delta( // the case where we want to round up to favor the pool. // Examples include: // - calculating amountIn during swap - // - adding liquidity (request user to provide more tokens in in favor of the pool) + // - adding liquidity (request user to provide more tokens in favor of the pool) const _liquidity = liquidity; const _diff = diff; return _liquidity.mul(_diff).roundUpDec(); diff --git a/packages/server/src/queries/complex/README.md b/packages/server/src/queries/complex/README.md index 6136199a373..e5ba13226af 100644 --- a/packages/server/src/queries/complex/README.md +++ b/packages/server/src/queries/complex/README.md @@ -16,7 +16,7 @@ With this, once a value is cached and the function is called again, the followin - The `ttl` is checked against the current time. If it's still live, it's returned. - If the current time has exceeded the `ttl` BUT NOT the `staleWhileRevalidate` time, the "stale" value is returned immediately, and a new value is created in the background for use in future requests. This ensures that the user does not experience a delay while the cache is being updated. -- If for whatever reason (often failed/slow query, a bug, etc.) the current time has exceeded `staleWhileRevalidate`, the cache is updated synchronously and the new value is returned. The user must wait for a new value to be created. This is often the case when the the underlying function is failing and the error is propagated to the user. +- If for whatever reason (often failed/slow query, a bug, etc.) the current time has exceeded `staleWhileRevalidate`, the cache is updated synchronously, and the new value is returned. The user must wait for a new value to be created. This is often the case when the underlying function is failing and the error is propagated to the user. Example: diff --git a/packages/server/src/queries/complex/assets/market.ts b/packages/server/src/queries/complex/assets/market.ts index a43d7df9708..dcf5708c5e8 100644 --- a/packages/server/src/queries/complex/assets/market.ts +++ b/packages/server/src/queries/complex/assets/market.ts @@ -18,7 +18,9 @@ import { DEFAULT_VS_CURRENCY } from "./config"; export type AssetMarketInfo = Partial<{ marketCap: PricePretty; currentPrice: PricePretty; + priceChange1h: RatePretty; priceChange24h: RatePretty; + priceChange7d: RatePretty; volume24h: PricePretty; }>; @@ -48,7 +50,9 @@ export async function getMarketAsset({ marketCap: marketCap ? new PricePretty(DEFAULT_VS_CURRENCY, marketCap) : undefined, + priceChange1h: assetMarketActivity?.price1hChange, priceChange24h: assetMarketActivity?.price24hChange, + priceChange7d: assetMarketActivity?.price7dChange, volume24h: assetMarketActivity?.volume24h, }; }, @@ -151,10 +155,18 @@ function makeMarketActivityFromTokenData(tokenData: TokenData) { ? new RatePretty(new Dec(tokenData.volume_24h_change).quo(new Dec(100))) : undefined, name: tokenData.name, + price1hChange: + tokenData.price_1h_change !== null + ? new RatePretty(new Dec(tokenData.price_1h_change).quo(new Dec(100))) + : undefined, price24hChange: tokenData.price_24h_change !== null ? new RatePretty(new Dec(tokenData.price_24h_change).quo(new Dec(100))) : undefined, + price7dChange: + tokenData.price_7d_change !== null + ? new RatePretty(new Dec(tokenData.price_7d_change).quo(new Dec(100))) + : undefined, exponent: tokenData.exponent, display: tokenData.display, }; diff --git a/packages/server/src/queries/complex/assets/price/historical.ts b/packages/server/src/queries/complex/assets/price/historical.ts index 53035e76433..f96ee6a9e07 100644 --- a/packages/server/src/queries/complex/assets/price/historical.ts +++ b/packages/server/src/queries/complex/assets/price/historical.ts @@ -33,7 +33,11 @@ export function getAssetHistoricalPrice({ timeFrame, numRecentFrames, }: { - /** Major (symbol) denom to fetch historical price data for. */ + /** + * Major (symbol) denom to fetch historical price data for. + * + * Note: this can be both a symbol or a denom (coinMinimalDenom) + * */ coinDenom: string; /** Number of minutes per bar. So 60 refers to price every hour. */ timeFrame: TimeFrame | CommonPriceChartTimeFrame; diff --git a/packages/server/src/queries/complex/transactions/transactions.ts b/packages/server/src/queries/complex/transactions/transactions.ts index 1d3d17e9c7f..d24852bd58b 100644 --- a/packages/server/src/queries/complex/transactions/transactions.ts +++ b/packages/server/src/queries/complex/transactions/transactions.ts @@ -42,56 +42,75 @@ export interface FormattedTransaction { const transactionsCache = new LRUCache(DEFAULT_LRU_OPTIONS); -// TODO - try / catch the getAssets - for v1 omit a specific trx if getAsset fails -// TODO - try / catch in the map +// TODO - v2 try / catch the getAssets when there's more data +// for v1 omit a specific trx if getAsset fails function mapMetadata( metadataArray: Metadata[], assetLists: AssetList[] ): FormattedMetadata[] { - return metadataArray.map((metadata) => ({ - ...metadata, - value: metadata.value.map((valueItem) => ({ - ...valueItem, - txFee: valueItem.txFee.map((fee) => ({ - token: new CoinPretty( - getAsset({ - assetLists, - anyDenom: fee.denom, - }), - fee.amount - ), - usd: new PricePretty(DEFAULT_VS_CURRENCY, fee.usd), - })), - txInfo: { - tokenIn: { - token: new CoinPretty( - getAsset({ - assetLists, - anyDenom: valueItem.txInfo.tokenIn.denom, - }), - valueItem.txInfo.tokenIn.amount - ), - usd: new PricePretty( - DEFAULT_VS_CURRENCY, - valueItem.txInfo.tokenIn.usd - ), - }, - tokenOut: { - token: new CoinPretty( - getAsset({ - assetLists, - anyDenom: valueItem.txInfo.tokenOut.denom, - }), - valueItem.txInfo.tokenOut.amount - ), - usd: new PricePretty( - DEFAULT_VS_CURRENCY, - valueItem.txInfo.tokenOut.usd - ), - }, - }, - })), - })); + return ( + metadataArray + .map((metadata) => { + try { + return { + ...metadata, + value: metadata.value.map((valueItem) => ({ + ...valueItem, + txFee: valueItem.txFee.map((fee) => { + try { + return { + token: new CoinPretty( + getAsset({ + assetLists, + anyDenom: fee?.denom, + }), + fee?.amount + ), + usd: new PricePretty(DEFAULT_VS_CURRENCY, fee?.usd), + }; + } catch (error) { + // TODO - clean up in v2 + throw new Error("Error mapping txFee"); + } + }), + txInfo: { + tokenIn: { + token: new CoinPretty( + getAsset({ + assetLists, + anyDenom: valueItem.txInfo.tokenIn?.denom, + }), + valueItem.txInfo.tokenIn?.amount + ), + usd: new PricePretty( + DEFAULT_VS_CURRENCY, + valueItem.txInfo.tokenIn?.usd + ), + }, + tokenOut: { + token: new CoinPretty( + getAsset({ + assetLists, + anyDenom: valueItem.txInfo.tokenOut?.denom, + }), + valueItem.txInfo.tokenOut?.amount + ), + usd: new PricePretty( + DEFAULT_VS_CURRENCY, + valueItem.txInfo.tokenOut?.usd + ), + }, + }, + })), + }; + } catch (error) { + // TODO - v2 add potential handler for error, v1 omit row + return null; + } + }) + // filter out any null values or values with empty arrays, indicating an error with getAsset + .filter((metadata) => metadata !== null) as FormattedMetadata[] + ); } export interface GetTransactionsResponse { @@ -142,8 +161,8 @@ export async function getTransactions({ // TODO - wrap getAsset with captureIfError - const mappedSwapTransactions = filteredSwapTransactions.map( - (transaction) => { + const mappedSwapTransactions = filteredSwapTransactions + .map((transaction) => { return { id: transaction._id, hash: transaction.hash, @@ -151,8 +170,12 @@ export async function getTransactions({ code: transaction.code, metadata: mapMetadata(transaction.metadata, assetLists), }; - } - ); + }) + // filter out transactions with no metadata / empty metadata + .filter( + (transaction) => + transaction.metadata && transaction.metadata.length > 0 + ); return { transactions: mappedSwapTransactions, diff --git a/packages/server/src/queries/data-services/token-data.ts b/packages/server/src/queries/data-services/token-data.ts index 81356acf6da..b392ca36b58 100644 --- a/packages/server/src/queries/data-services/token-data.ts +++ b/packages/server/src/queries/data-services/token-data.ts @@ -12,7 +12,9 @@ export interface TokenData { volume_24h: number; volume_24h_change: number | null; name: string; + price_1h_change: number | null; price_24h_change: number | null; + price_7d_change: number | null; exponent: number; display: string; } diff --git a/packages/server/src/queries/data-services/token-historical-chart.ts b/packages/server/src/queries/data-services/token-historical-chart.ts index a80a6e5e2df..14a27348611 100644 --- a/packages/server/src/queries/data-services/token-historical-chart.ts +++ b/packages/server/src/queries/data-services/token-historical-chart.ts @@ -42,14 +42,20 @@ export async function queryTokenHistoricalChart({ coinDenom, timeFrameMinutes, }: { - /** Major (symbol) denom to fetch historical price data for. */ + /** + * Major (symbol) denom to fetch historical price data for. + * + * Note: this can be both a symbol or a denom (coinMinimalDenom) + * */ coinDenom: string; /** Number of minutes per bar. So 60 refers to price every 60 minutes. */ timeFrameMinutes: TimeFrame; }): Promise { // collect params const url = new URL( - `/tokens/v2/historical/${coinDenom}/chart?tf=${timeFrameMinutes}`, + `/tokens/v2/historical/${encodeURIComponent( + coinDenom + )}/chart?tf=${timeFrameMinutes}`, TIMESERIES_DATA_URL ); try { diff --git a/packages/server/src/queries/twitter/twitter.ts b/packages/server/src/queries/twitter/twitter.ts index 9d686ffdefb..39c863d20a2 100644 --- a/packages/server/src/queries/twitter/twitter.ts +++ b/packages/server/src/queries/twitter/twitter.ts @@ -101,30 +101,37 @@ export class Twitter { * @returns An array of tweet's objects */ private async internalGetUserTweets(userId: string) { - const url = new URL( - `2/tweets/search/recent?query=${encodeURIComponent( - `from:${userId}` - )}&max_results=10&tweet.fields=created_at&expansions=author_id,attachments.media_keys&media.fields=media_key,type,url&user.fields=description,profile_image_url,url`, - TWITTER_API_URL - ); - - const { - data: tweets, - includes: { users, media }, - } = await apiClient<{ - data: RawTweet[]; - includes: { users: RawUser[]; media: RawMedia[] }; - }>(url.toString(), { - headers: { - Authorization: `Bearer ${TWITTER_API_ACCESS_TOKEN}`, - }, - }); + try { + const url = new URL( + `2/tweets/search/recent?query=${encodeURIComponent( + `from:${userId}` + )}&max_results=10&tweet.fields=created_at&expansions=author_id,attachments.media_keys&media.fields=media_key,type,url&user.fields=description,profile_image_url,url`, + TWITTER_API_URL + ); + + const { + data: tweets = [], + includes: { users, media } = { + users: [], + media: [], + }, + } = await apiClient<{ + data: RawTweet[]; + includes: { users: RawUser[]; media: RawMedia[] }; + }>(url.toString(), { + headers: { + Authorization: `Bearer ${TWITTER_API_ACCESS_TOKEN}`, + }, + }); - this.rawTweets = tweets; - this.rawUsers = users; - this.rawMedia = media; + this.rawTweets = tweets; + this.rawUsers = users; + this.rawMedia = media; - return this.tweets; + return this.tweets; + } catch { + return []; + } } /** diff --git a/packages/server/src/trpc-routers/__tests_e2e__/swap-router.test.ts b/packages/server/src/trpc-routers/__tests_e2e__/swap-router.test.ts index 23b7ced272d..3b58fcc4447 100644 --- a/packages/server/src/trpc-routers/__tests_e2e__/swap-router.test.ts +++ b/packages/server/src/trpc-routers/__tests_e2e__/swap-router.test.ts @@ -317,7 +317,7 @@ it("Sidecar — USDC.axl <> USDC — Should return valid quote for possible allo expect(reply.priceImpactTokenOut?.toDec().lte(new Dec(0.05))).toBeTruthy(); }); -it("Sidecar — ASTRO <> OSMO — Should return valid quote for PCL pool", async () => { +it.skip("Sidecar — ASTRO <> OSMO — Should return valid quote for PCL pool", async () => { const tokenInAmount = "1000000"; const tokenIn = astroAsset; const tokenOut = osmoAsset; diff --git a/packages/server/src/trpc-routers/assets-router.ts b/packages/server/src/trpc-routers/assets-router.ts index a5bd6f5455e..8ef9579da65 100644 --- a/packages/server/src/trpc-routers/assets-router.ts +++ b/packages/server/src/trpc-routers/assets-router.ts @@ -24,14 +24,18 @@ import { UserOsmoAddressSchema } from "../queries/complex/parameter-types"; import { AvailableRangeValues, AvailableTimeDurations, + TimeDuration, TimeFrame, } from "../queries/data-services"; -import { TimeDuration } from "../queries/data-services"; import { createTRPCRouter, publicProcedure } from "../trpc"; -import { captureErrorAndReturn } from "../utils/error"; -import { maybeCachePaginatedItems } from "../utils/pagination"; -import { createSortSchema, sort } from "../utils/sort"; -import { InfiniteQuerySchema } from "../utils/zod-types"; +import { + captureErrorAndReturn, + compareCommon, + createSortSchema, + InfiniteQuerySchema, + maybeCachePaginatedItems, + sort, +} from "../utils"; const GetInfiniteAssetsInputSchema = InfiniteQuerySchema.merge(AssetFilterSchema); @@ -176,10 +180,13 @@ export const assetsRouter = createTRPCRouter({ z.object({ sort: createSortSchema([ "currentPrice", + "priceChange1h", "priceChange24h", + "priceChange7d", "marketCap", "volume24h", ] as const).optional(), + watchListDenoms: z.array(z.string()).optional(), }) ) ) @@ -189,6 +196,7 @@ export const assetsRouter = createTRPCRouter({ search, onlyVerified, sort: sortInput, + watchListDenoms, categories, cursor, limit, @@ -198,7 +206,7 @@ export const assetsRouter = createTRPCRouter({ }) => maybeCachePaginatedItems({ getFreshItems: async () => { - let assets = await mapGetMarketAssets({ + const assets = await mapGetMarketAssets({ ...ctx, search, onlyVerified, @@ -206,17 +214,40 @@ export const assetsRouter = createTRPCRouter({ categories, }); + // sorting if (sortInput) { - assets = sort(assets, sortInput.keyPath, sortInput.direction); - } + // user sorting + + return sort(assets, sortInput.keyPath, sortInput.direction); + } else { + // default sorting, maybe with watchlist - // Can be searching and/or sorting - return assets; + if (watchListDenoms) { + // default sort watchlist to top + return assets.sort((a, b) => { + // 1. watchlist denoms sorted by volume 24h desc + if ( + watchListDenoms.includes(a.coinDenom) && + watchListDenoms.includes(b.coinDenom) + ) + return compareCommon(a.volume24h, b.volume24h); + if (watchListDenoms.includes(a.coinDenom)) return -1; + if (watchListDenoms.includes(b.coinDenom)) return 1; + + // 2. rest of the assets by volume 24h desc + return compareCommon(a.volume24h, b.volume24h); + }); + } else { + // default sort by volume24h desc + return sort(assets, "volume24h"); + } + } }, cacheKey: JSON.stringify({ search, onlyVerified, sort: sortInput, + watchListDenoms, categories, includePreview, }), @@ -420,10 +451,6 @@ export const assetsRouter = createTRPCRouter({ coinMinimalDenom: asset.coinMinimalDenom, }); - if (listingDate) { - console.log("listingDate", asset.coinDenom, listingDate); - } - return { ...asset, listingDate, diff --git a/packages/server/src/utils/__tests__/sort.spec.ts b/packages/server/src/utils/__tests__/sort.spec.ts index 2fcd36ba6cc..94cb411d876 100644 --- a/packages/server/src/utils/__tests__/sort.spec.ts +++ b/packages/server/src/utils/__tests__/sort.spec.ts @@ -39,32 +39,6 @@ describe("sort function", () => { expect(result).toEqual(list); }); - it("should sort using a custom compare function", () => { - const list = [ - { name: "John", age: 30 }, - { name: "Alice", age: 20 }, - { name: "Bob", age: 25 }, - ]; - - const resultAsc = sort(list, "name", "asc", (a, b) => - a.name.localeCompare(b.name) - ); - expect(resultAsc).toEqual([ - { name: "Alice", age: 20 }, - { name: "Bob", age: 25 }, - { name: "John", age: 30 }, - ]); - - const resultDesc = sort(list, "name", "desc", (a, b) => - b.name.localeCompare(a.name) - ); - expect(resultDesc).toEqual([ - { name: "John", age: 30 }, - { name: "Bob", age: 25 }, - { name: "Alice", age: 20 }, - ]); - }); - it("should sort using a deep key path", () => { const list = [ { name: "John", age: { b: 20 } }, diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 1337a34125b..c9990b8551f 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -1,5 +1,6 @@ export * from "./async"; export * from "./cache"; +export * from "./compare"; export * from "./error"; export * from "./pagination"; export * from "./search"; diff --git a/packages/server/src/utils/sort.ts b/packages/server/src/utils/sort.ts index 603452bbb8d..04390776c0c 100644 --- a/packages/server/src/utils/sort.ts +++ b/packages/server/src/utils/sort.ts @@ -28,14 +28,11 @@ export function createSortSchema< /** Sorts a list of objects by given sort params - a key and sort direction - into a new array. * Includes handling for common complex types like Dec, Int, and it's *Pretty counterparts. * Filters elements at the given `keyPath` that are `null` or `undefined`. - * Includes a custom compare function for sorting any other types which will override - * default behavior including the sort direction. * Default `direction` is `"desc"`. */ export function sort>( list: TItem[], keyPath: string, - direction: SortDirection = "desc", - compare?: (a: TItem, b: TItem) => number + direction: SortDirection = "desc" ): TItem[] { // list is empty or keyPath is not in the list items, return items if (list.length === 0 || !getValueAtPath(list[0], keyPath)) { @@ -52,8 +49,6 @@ export function sort>( ? getValueAtPath(b, keyPath) : b[keyPath]; - if (compare) return compare(a, b); - const commonCompare = withDirection( compareCommon(aValue, bValue), direction diff --git a/packages/stores/src/account/__tests_e2e__/swap-exact-in-positions.spec.ts b/packages/stores/src/account/__tests_e2e__/swap-exact-in-positions.spec.ts index b2e78f66a6f..8123e7ba7a9 100644 --- a/packages/stores/src/account/__tests_e2e__/swap-exact-in-positions.spec.ts +++ b/packages/stores/src/account/__tests_e2e__/swap-exact-in-positions.spec.ts @@ -712,7 +712,7 @@ describe("Test Swap Exact In - Concentrated Liquidity", () => { return result; } - // Estimates amount one out and amount zero in in necessary to swap to a specific tick + // Estimates amount one out and amount zero in necessary to swap to a specific tick // Note: correctness of strategy is assumed function estimateAmountOneOutZeroInToTick( tickToSwapTo: Int, diff --git a/packages/types/src/asset-types.ts b/packages/types/src/asset-types.ts index 2b40bdda22c..bfb063d5a7b 100644 --- a/packages/types/src/asset-types.ts +++ b/packages/types/src/asset-types.ts @@ -90,7 +90,7 @@ export interface CosmosCounterparty { sourceDenom: string; symbol: string; decimals: number; - logoURIs: LogoURIs; + logoURIs?: LogoURIs; } export interface EVMCounterparty { @@ -101,7 +101,7 @@ export interface EVMCounterparty { address: string; symbol: string; decimals: number; - logoURIs: LogoURIs; + logoURIs?: LogoURIs; } export interface NonCosmosCounterparty { @@ -110,7 +110,7 @@ export interface NonCosmosCounterparty { sourceDenom: string; decimals: number; symbol: string; - logoURIs: LogoURIs; + logoURIs?: LogoURIs; } export type Counterparty = diff --git a/packages/web/README.md b/packages/web/README.md index 63e3ab9b284..dd534c98f04 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -109,6 +109,18 @@ It returns `unknown` types, so you may need to type cast to resolve TS errors. To install Playwright, please execute `npx playwright install` from the /web folder. -To run E2E tests, please execute `npx playwright test -g "Test Select Swap Pair feature"` from the /web folder. +To run Select pair tests, please execute `npx playwright test -g "Test Select Swap Pair feature"` from the /web folder. +To run Swap E2E tests, please execute `npx playwright test -g "Test Swap feature"` from the /web folder. Tests can be executed locally in a browser by changing `headless: true` to `headless: false`. + +## GitHub E2E Tests workflow + +[Tests Workflow](https://github.com/osmosis-labs/osmosis-frontend/blob/stage/.github/workflows/frontend-e2e-tests.yml) is initiated on `deployment_status` event from the Vercel bot. +It contains 2 jobs: + +- Mainnet tests on condition `github.event.deployment_status.environment == 'Production – osmosis-frontend'` +- Preview tests on condition `github.event.deployment_status.environment == 'Preview – osmosis-frontend'` +- Test report is uploaded as Artifact + +![Screenshot 2024-05-03 at 20 06 53](https://github.com/osmosis-labs/osmosis-frontend/assets/62520712/ce00bfac-24a6-429d-ac4e-7dacbbcb8efd) diff --git a/packages/web/components/assets/categories.tsx b/packages/web/components/assets/categories.tsx index 57140d860ef..5cda45ed577 100644 --- a/packages/web/components/assets/categories.tsx +++ b/packages/web/components/assets/categories.tsx @@ -318,7 +318,7 @@ const OverlappingAssetImages: FunctionComponent< key={url} style={{ marginLeft: `${index * 24}px`, - zIndex: 50 + index, + zIndex: 40 + index, }} className={classNames( "absolute flex h-8 w-8 items-center justify-center", diff --git a/packages/web/components/assets/highlights-categories.tsx b/packages/web/components/assets/highlights-categories.tsx index 1f89aad4e12..4ea063383bd 100644 --- a/packages/web/components/assets/highlights-categories.tsx +++ b/packages/web/components/assets/highlights-categories.tsx @@ -5,7 +5,13 @@ import { FunctionComponent, ReactNode } from "react"; import { PriceChange } from "~/components/assets/price"; import SkeletonLoader from "~/components/loaders/skeleton-loader"; -import { Breakpoint, useTranslation, useWindowSize } from "~/hooks"; +import { EventName } from "~/config"; +import { + Breakpoint, + useAmplitudeAnalytics, + useTranslation, + useWindowSize, +} from "~/hooks"; import { api, RouterOutputs } from "~/utils/trpc"; import { CustomClasses } from "../types"; @@ -17,9 +23,11 @@ type PriceChange24hAsset = type UpcomingReleaseAsset = RouterOutputs["edge"]["assets"]["getTopUpcomingAssets"][number]; +type Highlight = "new" | "topGainers" | "upcoming"; + type HighlightsProps = { isCategorySelected: boolean; - onSelectCategory: (category: string) => void; + onSelectCategory: (category: string, highlight: Highlight) => void; onSelectAllTopGainers: () => void; } & CustomClasses; @@ -66,20 +74,24 @@ const HighlightsGrid: FunctionComponent = ({ title={t("assets.highlights.new")} isLoading={isTopNewAssetsLoading} assets={(topNewAssets ?? []).map(highlightPrice24hChangeAsset)} - onClickSeeAll={() => onSelectCategory("new")} + onClickSeeAll={() => onSelectCategory("new", "new")} + highlight="new" /> ); @@ -122,6 +134,7 @@ function highlightUpcomingReleaseAsset(asset: UpcomingReleaseAsset) { export const AssetHighlights: FunctionComponent< { title: string; + subtitle?: string; onClickSeeAll?: () => void; assets: { asset: { @@ -133,8 +146,17 @@ export const AssetHighlights: FunctionComponent< }[]; isLoading?: boolean; disableLinking?: boolean; + highlight: Highlight; } & CustomClasses -> = ({ title, onClickSeeAll, assets, isLoading = false, className }) => { +> = ({ + title, + subtitle, + onClickSeeAll, + assets, + isLoading = false, + className, + highlight, +}) => { const { t } = useTranslation(); return ( @@ -145,7 +167,12 @@ export const AssetHighlights: FunctionComponent< )} >
-
{title}
+
+ {title}{" "} + {subtitle && ( + {subtitle} + )} +
{onClickSeeAll && ( +
+ ); +}; + export const AssetsPageV1: FunctionComponent = observer(() => { const { isMobile } = useWindowSize(); const { assetsStore } = useStore(); @@ -70,9 +89,15 @@ export const AssetsPageV1: FunctionComponent = observer(() => { [bridgeAsset] ); + const flags = useFeatureFlags(); + return (
- +
+ + {flags.transactionsPage && } +
+ {ownedPoolIds.length > INIT_POOL_CARD_COUNT && ( - { - setShowAllPools(!showAllPools); - }} - /> +
+ { + setShowAllPools(!showAllPools); + }} + /> +
)} )); diff --git a/packages/web/components/funnels/balances-moved.tsx b/packages/web/components/funnels/balances-moved.tsx index 98f4289a732..42c2e43552e 100644 --- a/packages/web/components/funnels/balances-moved.tsx +++ b/packages/web/components/funnels/balances-moved.tsx @@ -1,6 +1,6 @@ import classNames from "classnames"; import Image from "next/image"; -import { useRouter } from "next/router"; +import Link from "next/link"; import { forwardRef } from "react"; import { useLocalStorage } from "react-use"; @@ -9,10 +9,9 @@ import { useTranslation } from "~/hooks"; import { Icon } from "../assets"; import { CustomClasses } from "../types"; -export const BalancesMoved = forwardRef( +export const BalancesMoved = forwardRef( function BalancesMoved({ className }, ref) { const { t } = useTranslation(); - const router = useRouter(); const [isClosed, setIsClosed] = useLocalStorage( "assets-page-balances-moved-user-ack", @@ -24,13 +23,13 @@ export const BalancesMoved = forwardRef( } return ( -
router.push("/portfolio")} >
(
-
+ ); } ); diff --git a/packages/web/components/swap-tool/index.tsx b/packages/web/components/swap-tool/index.tsx index 65d9956d2c7..885757105e5 100644 --- a/packages/web/components/swap-tool/index.tsx +++ b/packages/web/components/swap-tool/index.tsx @@ -188,6 +188,10 @@ export const SwapTool: FunctionComponent = observer( ({ pools }) => pools.length !== 1 ), isMultiRoute: (swapState.quote?.split.length ?? 0) > 1, + valueUsd: Number( + swapState.tokenOutFiatValue?.toDec().toString() ?? "0" + ), + page, }; logEvent([ EventName.Swap.swapStarted, @@ -195,7 +199,6 @@ export const SwapTool: FunctionComponent = observer( ...baseEvent, quoteTimeMilliseconds: swapState.quote?.timeMs, router: swapState.quote?.name, - page, }, ]); swapState @@ -209,8 +212,6 @@ export const SwapTool: FunctionComponent = observer( isMultiHop: result === "multihop", quoteTimeMilliseconds: swapState.quote?.timeMs, router: swapState.quote?.name, - page, - valueUsd: Number(swapState.tokenOutFiatValue?.toString() ?? "0"), }, ]); @@ -917,9 +918,9 @@ export const SwapTool: FunctionComponent = observer(