From 5e625ed184355c341f2710aec24ac7d69ff0e87e Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Mon, 28 Oct 2024 22:15:04 +0100 Subject: [PATCH 01/94] [ECO-2331] Update meta description (#311) Co-authored-by: alnoki <43892045+alnoki@users.noreply.github.com> --- .../frontend/src/app/launch/page.tsx | 7 +++++ .../frontend/src/app/market/[market]/page.tsx | 29 +++++++++++++++++++ .../frontend/src/app/pools/page.tsx | 7 +++++ src/typescript/frontend/src/configs/meta.ts | 8 +++-- src/typescript/frontend/src/utils/index.ts | 4 +++ 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/typescript/frontend/src/app/launch/page.tsx b/src/typescript/frontend/src/app/launch/page.tsx index ba7c4ad61..8675917de 100644 --- a/src/typescript/frontend/src/app/launch/page.tsx +++ b/src/typescript/frontend/src/app/launch/page.tsx @@ -2,10 +2,17 @@ import { REVALIDATION_TIME } from "lib/server-env"; import ClientLaunchEmojicoinPage from "../../components/pages/launch-emojicoin/ClientLaunchEmojicoinPage"; import { isUserGeoblocked } from "utils/geolocation"; import { headers } from "next/headers"; +import { type Metadata } from "next"; +import { emoji } from "utils"; export const revalidate = REVALIDATION_TIME; export const dynamic = "force-static"; +export const metadata: Metadata = { + title: "launch", + description: `Launch your own emojicoins using emojicoin.fun ${emoji("party popper")}`, +}; + export default async function LaunchEmojicoinPage() { const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); return ; diff --git a/src/typescript/frontend/src/app/market/[market]/page.tsx b/src/typescript/frontend/src/app/market/[market]/page.tsx index df714de8e..0a71e5dc9 100644 --- a/src/typescript/frontend/src/app/market/[market]/page.tsx +++ b/src/typescript/frontend/src/app/market/[market]/page.tsx @@ -8,6 +8,7 @@ import { isUserGeoblocked } from "utils/geolocation"; import { headers } from "next/headers"; import { fetchChatEvents, fetchMarketState, fetchSwapEvents } from "@/queries/market"; import { deriveEmojicoinPublisherAddress } from "@sdk/emojicoin_dot_fun"; +import { type Metadata } from "next"; export const revalidate = REVALIDATION_TIME; export const dynamic = "force-dynamic"; @@ -26,6 +27,34 @@ interface EmojicoinPageProps { searchParams: {}; } +export async function generateMetadata({ params }: EmojicoinPageProps): Promise { + const { market: marketSlug } = params; + const names = pathToEmojiNames(marketSlug); + const emojis = names.map((n) => { + const res = SYMBOL_EMOJI_DATA.byName(n)?.emoji; + if (!res) { + throw new Error(`Cannot parse invalid emoji input: ${marketSlug}, names: ${names}`); + } + return res; + }); + + const title = `${emojis.join("")}`; + const description = `Trade ${emojis.join("")} on emojicoin.fun !`; + + return { + title, + description, + openGraph: { + title, + description, + }, + twitter: { + title, + description, + }, + }; +} + /** * Note that our queries work with the marketID, but the URL uses the emoji bytes with a URL encoding. * That is, if you paste the emoji 💅🏾 into the URL, it becomes %F0%9F%92%85%F0%9F%8F%BE. diff --git a/src/typescript/frontend/src/app/pools/page.tsx b/src/typescript/frontend/src/app/pools/page.tsx index 943a855a3..f1eb01115 100644 --- a/src/typescript/frontend/src/app/pools/page.tsx +++ b/src/typescript/frontend/src/app/pools/page.tsx @@ -5,10 +5,17 @@ import { isUserGeoblocked } from "utils/geolocation"; import { getPoolData } from "./api/getPoolDataQuery"; import { SortMarketsBy } from "@sdk/indexer-v2/types/common"; import { symbolBytesToEmojis } from "@sdk/emoji_data/utils"; +import { type Metadata } from "next"; +import { emoji } from "utils"; export const revalidate = REVALIDATION_TIME; export const dynamic = "force-dynamic"; +export const metadata: Metadata = { + title: "pools", + description: `Provide ${emoji("water wave")}liquidity${emoji("water wave")} and earn APR using your emojis !`, +}; + export default async function PoolsPage({ searchParams }: { searchParams: { pool: string } }) { const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); const initialData = await getPoolData( diff --git a/src/typescript/frontend/src/configs/meta.ts b/src/typescript/frontend/src/configs/meta.ts index 2a144f083..3fa68f1f0 100644 --- a/src/typescript/frontend/src/configs/meta.ts +++ b/src/typescript/frontend/src/configs/meta.ts @@ -1,7 +1,8 @@ import { type Metadata } from "next"; +import { emoji } from "utils"; export const DEFAULT_TITLE = "emojicoin.fun"; -export const DEFAULT_DESCRIPTION = "Emojicoin joy starts here"; +export const DEFAULT_DESCRIPTION = `Give your wallet the personality it deserves ${emoji("zany face")}${emoji("sparkles")}`; export const OG_IMAGES = "/social-preview.png"; export const OG_TYPE = "website"; export const TWITTER_CARD = "summary"; @@ -27,7 +28,10 @@ export const getDefaultMetadata = (): Metadata => { alternates: { canonical: "/", }, - title: DEFAULT_TITLE, + title: { + default: DEFAULT_TITLE, + template: `%s | ${DEFAULT_TITLE}`, + }, description: DEFAULT_DESCRIPTION, keywords: "aptos, tokens, emoji, emojicoins", openGraph: { diff --git a/src/typescript/frontend/src/utils/index.ts b/src/typescript/frontend/src/utils/index.ts index 03c29c1de..f1947cdf7 100644 --- a/src/typescript/frontend/src/utils/index.ts +++ b/src/typescript/frontend/src/utils/index.ts @@ -1,3 +1,5 @@ +import { SYMBOL_EMOJI_DATA } from "@sdk/emoji_data"; + export { checkIsEllipsis } from "./check-is-ellipsis"; export { getFileNameFromSrc } from "./get-file-name-from-src"; export { @@ -20,3 +22,5 @@ export const parseJSON = (json: string): T => } return value as T; }); + +export const emoji = (name: string): string => SYMBOL_EMOJI_DATA.byName(name)!.emoji; From 4d1721f18ba5918530f78e35bd61d4519e26ea3e Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Wed, 30 Oct 2024 11:48:31 +0100 Subject: [PATCH 02/94] [ECO-2056] Add e2e search test (#309) --- cfg/cspell-frontend-dictionary.txt | 1 + src/docker/compose.yaml | 10 ++--- src/docker/utils/prune.sh | 4 +- src/typescript/ci.env | 1 + src/typescript/frontend/.eslintrc.js | 2 +- src/typescript/frontend/package.json | 1 + .../emoji-picker/EmojiPickerWithInput.tsx | 1 + src/typescript/frontend/src/lib/server-env.ts | 6 ++- src/typescript/frontend/src/middleware.ts | 5 +++ .../frontend/tests/e2e/global.setup.ts | 2 +- .../frontend/tests/e2e/global.teardown.ts | 2 +- .../frontend/tests/e2e/search.spec.ts | 23 +++++++++++ src/typescript/pnpm-lock.yaml | 3 ++ .../sdk/src/client/emojicoin-client.ts | 39 ++++++++++++++++++- .../sdk/src/indexer-v2/queries/app/home.ts | 4 +- .../sdk/src/indexer-v2/queries/app/market.ts | 4 +- .../sdk/src/indexer-v2/queries/client.ts | 4 +- .../sdk/src/indexer-v2/queries/utils.ts | 4 +- src/typescript/sdk/src/queries/index.ts | 4 +- .../utils/test/docker/docker-test-harness.ts | 24 +++++++++--- src/typescript/sdk/tests/post-test.ts | 2 +- src/typescript/sdk/tests/pre-test.ts | 2 +- 22 files changed, 124 insertions(+), 24 deletions(-) create mode 100644 src/typescript/frontend/tests/e2e/search.spec.ts diff --git a/cfg/cspell-frontend-dictionary.txt b/cfg/cspell-frontend-dictionary.txt index c814a2cc8..55b0795a2 100644 --- a/cfg/cspell-frontend-dictionary.txt +++ b/cfg/cspell-frontend-dictionary.txt @@ -50,3 +50,4 @@ localstorage vpnapi ctrls dockerenv +testid diff --git a/src/docker/compose.yaml b/src/docker/compose.yaml index fa7e7fe6d..bd7cd36a3 100644 --- a/src/docker/compose.yaml +++ b/src/docker/compose.yaml @@ -100,13 +100,13 @@ services: dockerfile: 'src/docker/frontend/Dockerfile' args: HASH_SEED: '${HASH_SEED}' - EMOJICOIN_INDEXER_URL: 'http://localhost:3000' + EMOJICOIN_INDEXER_URL: 'http://host.docker.internal:3000' NEXT_PUBLIC_APTOS_NETWORK: '${APTOS_NETWORK}' NEXT_PUBLIC_INTEGRATOR_ADDRESS: '${EMOJICOIN_INTEGRATOR_ADDRESS}' NEXT_PUBLIC_INTEGRATOR_FEE_RATE_BPS: '${FEE_RATE_BPS}' NEXT_PUBLIC_IS_ALLOWLIST_ENABLED: 'false' NEXT_PUBLIC_MODULE_ADDRESS: '${EMOJICOIN_MODULE_ADDRESS}' - NEXT_PUBLIC_BROKER_URL: 'ws://localhost:${BROKER_PORT}' + NEXT_PUBLIC_BROKER_URL: 'ws://host.docker.internal:${BROKER_PORT}' NEXT_PUBLIC_REWARDS_MODULE_ADDRESS: >- ${EMOJICOIN_REWARDS_MODULE_ADDRESS} REVALIDATION_TIME: '${REVALIDATION_TIME}' @@ -116,18 +116,18 @@ services: - 'frontend' environment: HASH_SEED: '${HASH_SEED}' - EMOJICOIN_INDEXER_URL: 'http://localhost:3000' + EMOJICOIN_INDEXER_URL: 'http://host.docker.internal:3000' NEXT_PUBLIC_APTOS_NETWORK: '${APTOS_NETWORK}' NEXT_PUBLIC_INTEGRATOR_ADDRESS: '${EMOJICOIN_INTEGRATOR_ADDRESS}' NEXT_PUBLIC_INTEGRATOR_FEE_RATE_BPS: '${FEE_RATE_BPS}' NEXT_PUBLIC_IS_ALLOWLIST_ENABLED: 'false' NEXT_PUBLIC_MODULE_ADDRESS: '${EMOJICOIN_MODULE_ADDRESS}' - NEXT_PUBLIC_BROKER_URL: 'ws://localhost:${BROKER_PORT}' + NEXT_PUBLIC_BROKER_URL: 'ws://host.docker.internal:${BROKER_PORT}' NEXT_PUBLIC_REWARDS_MODULE_ADDRESS: '${EMOJICOIN_REWARDS_MODULE_ADDRESS}' REVALIDATION_TIME: '${REVALIDATION_TIME}' healthcheck: test: 'curl -f http://localhost:3001/ || exit 1' - interval: '2m' + interval: '30s' timeout: '1s' retries: '1' start_period: '20s' diff --git a/src/docker/utils/prune.sh b/src/docker/utils/prune.sh index 81f93b284..133a10c59 100755 --- a/src/docker/utils/prune.sh +++ b/src/docker/utils/prune.sh @@ -137,7 +137,7 @@ if [ -n "$reset_localnet" ]; then # Bind-mounting the parent of `.aptos` gives the container the right to # delete it. docker run --rm -v "$docker_dir/localnet:/pwd" busybox rm -rf /pwd/.aptos - docker compose -f compose.local.yaml down --volumes + docker compose -f compose.local.yaml --profile frontend down --volumes else - docker compose -f compose.local.yaml down + docker compose -f compose.local.yaml --profile frontend down fi diff --git a/src/typescript/ci.env b/src/typescript/ci.env index 6fc7c9a20..3d3bff668 100644 --- a/src/typescript/ci.env +++ b/src/typescript/ci.env @@ -1,3 +1,4 @@ +NODE_ENV="test" APTOS_NETWORK="local" NEXT_PUBLIC_APTOS_NETWORK="local" NEXT_PUBLIC_BROKER_URL="ws://localhost:3009" diff --git a/src/typescript/frontend/.eslintrc.js b/src/typescript/frontend/.eslintrc.js index 335c5f743..46734c91f 100644 --- a/src/typescript/frontend/.eslintrc.js +++ b/src/typescript/frontend/.eslintrc.js @@ -25,7 +25,7 @@ module.exports = { "playwright.config.ts", "postcss.config.js", "tailwind.config.js", - "example.spec.ts", + "tests/**", ], parser: "@typescript-eslint/parser", parserOptions: { diff --git a/src/typescript/frontend/package.json b/src/typescript/frontend/package.json index afaef7fb9..ab91ee419 100644 --- a/src/typescript/frontend/package.json +++ b/src/typescript/frontend/package.json @@ -35,6 +35,7 @@ "axios": ">=0.28.0", "big.js": "^6.2.2", "clsx": "^2.1.1", + "dotenv": "^16.4.5", "emoji-mart": "https://github.com/econia-labs/emoji-mart/raw/emojicoin-dot-fun/packages/emoji-mart/emoji-mart-v5.6.0.tgz", "emoji-regex": "^10.4.0", "framer-motion": "^11.11.4", diff --git a/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx b/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx index 725c329bd..136c373ac 100644 --- a/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx +++ b/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx @@ -250,6 +250,7 @@ export const EmojiPickerWithInput = ({ onClick={() => { setPickerInvisible(false); }} + data-testid="emoji-input" /> {mode === "search" && close} {mode === "chat" ? ( diff --git a/src/typescript/frontend/src/lib/server-env.ts b/src/typescript/frontend/src/lib/server-env.ts index d4fc10c53..451be828f 100644 --- a/src/typescript/frontend/src/lib/server-env.ts +++ b/src/typescript/frontend/src/lib/server-env.ts @@ -43,7 +43,11 @@ export const GALXE_CAMPAIGN_ID: string | undefined = process.env.GALXE_CAMPAIGN_ export const REVALIDATION_TIME: number = Number(process.env.REVALIDATION_TIME); export const VPNAPI_IO_API_KEY: string = process.env.VPNAPI_IO_API_KEY!; -if (APTOS_NETWORK === Network.LOCAL && !EMOJICOIN_INDEXER_URL.includes("localhost")) { +if ( + APTOS_NETWORK === Network.LOCAL && + !EMOJICOIN_INDEXER_URL.includes("localhost") && + !EMOJICOIN_INDEXER_URL.includes("docker") +) { throw new Error( `APTOS_NETWORK is ${APTOS_NETWORK} but the indexer processor url is set to ${EMOJICOIN_INDEXER_URL}` ); diff --git a/src/typescript/frontend/src/middleware.ts b/src/typescript/frontend/src/middleware.ts index 3bc7b37c1..55350fbb0 100644 --- a/src/typescript/frontend/src/middleware.ts +++ b/src/typescript/frontend/src/middleware.ts @@ -3,6 +3,7 @@ import { COOKIE_FOR_HASHED_ADDRESS, } from "components/pages/verify/session-info"; import { authenticate } from "components/pages/verify/verify"; +import { IS_ALLOWLIST_ENABLED } from "lib/env"; import { NextResponse, type NextRequest } from "next/server"; import { ROUTES } from "router/routes"; import { normalizePossibleMarketPath } from "utils/pathname-helpers"; @@ -24,6 +25,10 @@ export default async function middleware(request: NextRequest) { return NextResponse.redirect(possibleMarketPath); } + if (!IS_ALLOWLIST_ENABLED) { + return NextResponse.next(); + } + const hashed = request.cookies.get(COOKIE_FOR_HASHED_ADDRESS)?.value; const address = request.cookies.get(COOKIE_FOR_ACCOUNT_ADDRESS)?.value; diff --git a/src/typescript/frontend/tests/e2e/global.setup.ts b/src/typescript/frontend/tests/e2e/global.setup.ts index d8ad39b39..d9ef2c102 100644 --- a/src/typescript/frontend/tests/e2e/global.setup.ts +++ b/src/typescript/frontend/tests/e2e/global.setup.ts @@ -6,6 +6,6 @@ setup("setup the Docker containers", async ({}) => { setup.setTimeout(600_000); const startDockerServices = process.env.APTOS_NETWORK === "local"; if (startDockerServices) { - await DockerTestHarness.run(true); + await DockerTestHarness.run({ frontend: true }); } }); diff --git a/src/typescript/frontend/tests/e2e/global.teardown.ts b/src/typescript/frontend/tests/e2e/global.teardown.ts index f22043bd2..f77c20353 100644 --- a/src/typescript/frontend/tests/e2e/global.teardown.ts +++ b/src/typescript/frontend/tests/e2e/global.teardown.ts @@ -2,5 +2,5 @@ import { DockerTestHarness } from "../../../sdk/src/utils/test/docker/docker-test-harness"; export default async function postTest() { - await DockerTestHarness.stop(); + await DockerTestHarness.stop({ frontend: true }); } diff --git a/src/typescript/frontend/tests/e2e/search.spec.ts b/src/typescript/frontend/tests/e2e/search.spec.ts new file mode 100644 index 000000000..0ea3b782d --- /dev/null +++ b/src/typescript/frontend/tests/e2e/search.spec.ts @@ -0,0 +1,23 @@ +import { test, expect } from "@playwright/test"; +import { EmojicoinClient } from "../../../sdk/src/client/emojicoin-client"; +import { getFundedAccount } from "../../../sdk/src/utils/test/test-accounts"; +import { SYMBOL_EMOJI_DATA } from "../../../sdk/src"; + +test("check search results", async ({ page }) => { + const user = getFundedAccount("666"); + const symbols = [SYMBOL_EMOJI_DATA.byName("cat")!.emoji, SYMBOL_EMOJI_DATA.byName("cat")!.emoji]; + const client = new EmojicoinClient(); + await client.register(user, symbols).then((res) => res.handle); + + await page.goto("/home"); + + const search = page.getByTestId("emoji-input"); + expect(search).toBeVisible(); + await search.fill(symbols.join("")); + + const marketCard = page.getByText("cat,cat", { exact: true }); + expect(marketCard).toBeVisible(); + await marketCard.click(); + + await expect(page).toHaveURL(/.*cat;cat/); +}); diff --git a/src/typescript/pnpm-lock.yaml b/src/typescript/pnpm-lock.yaml index c2917b1c7..4f5899249 100644 --- a/src/typescript/pnpm-lock.yaml +++ b/src/typescript/pnpm-lock.yaml @@ -99,6 +99,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 emoji-mart: specifier: https://github.com/econia-labs/emoji-mart/raw/emojicoin-dot-fun/packages/emoji-mart/emoji-mart-v5.6.0.tgz version: https://github.com/econia-labs/emoji-mart/raw/emojicoin-dot-fun/packages/emoji-mart/emoji-mart-v5.6.0.tgz diff --git a/src/typescript/sdk/src/client/emojicoin-client.ts b/src/typescript/sdk/src/client/emojicoin-client.ts index 44f7d1d69..da8b67a60 100644 --- a/src/typescript/sdk/src/client/emojicoin-client.ts +++ b/src/typescript/sdk/src/client/emojicoin-client.ts @@ -24,7 +24,10 @@ import { type EventsModels, getEventsAsProcessorModelsFromResponse } from "../mi import { getAptosClient } from "../utils/aptos-client"; import { toChatMessageEntryFunctionArgs } from "../emoji_data"; import customExpect from "./expect"; -import { INTEGRATOR_ADDRESS } from "../const"; +import { DEFAULT_REGISTER_MARKET_GAS_OPTIONS, INTEGRATOR_ADDRESS } from "../const"; +import { waitFor } from "../utils"; +import { postgrest } from "../indexer-v2/queries"; +import { TableName } from "../indexer-v2/types/json-types"; const { expect, Expect } = customExpect; @@ -34,6 +37,27 @@ type Options = { waitForTransactionOptions?: WaitForTransactionOptions; }; +const waitForEventProcessed = async ( + marketID: bigint, + marketNonce: bigint, + tableName: TableName +) => { + return waitFor({ + condition: async () => { + const data = await postgrest + .from(tableName) + .select("*") + .eq("market_id", marketID) + .eq("market_nonce", marketNonce); + return data.error === null && data.data?.length === 1; + }, + interval: 500, + maxWaitTime: 30000, + throwError: true, + errorMessage: "Event did not register on time.", + }); +}; + /** * A helper class intended to streamline the process of submitting transactions and using utility * functions for emojis and market symbols. @@ -118,15 +142,18 @@ export class EmojicoinClient { registrant, emojis: this.emojisToHexStrings(symbolEmojis), integrator: this.integrator, + options: DEFAULT_REGISTER_MARKET_GAS_OPTIONS, ...options, }); const res = this.getTransactionEventData(response); + const marketID = res.events.marketRegistrationEvents[0].marketID; return { ...res, registration: { event: expect(res.events.marketRegistrationEvents.at(0), Expect.Register.Event), model: expect(res.models.marketRegistrationEvents.at(0), Expect.Register.Model), }, + handle: waitForEventProcessed(marketID, 1n, TableName.MarketRegistrationEvents), }; } @@ -150,12 +177,14 @@ export class EmojicoinClient { ...this.getEmojicoinInfo(symbolEmojis), }); const res = this.getTransactionEventData(response); + const { emitMarketNonce, marketID } = res.events.chatEvents[0]; return { ...res, chat: { event: expect(res.events.chatEvents.at(0), Expect.Chat.Event), model: expect(res.models.chatEvents.at(0), Expect.Chat.Model), }, + handle: waitForEventProcessed(marketID, emitMarketNonce, TableName.ChatEvents), }; } @@ -219,12 +248,14 @@ export class EmojicoinClient { ...this.getEmojicoinInfo(symbolEmojis), }); const res = this.getTransactionEventData(response); + const { marketNonce, marketID } = res.events.swapEvents[0]; return { ...res, swap: { event: expect(res.events.swapEvents.at(0), Expect.Swap.Event), model: expect(res.models.swapEvents.at(0), Expect.Swap.Model), }, + handle: waitForEventProcessed(marketID, marketNonce, TableName.SwapEvents), }; } @@ -274,12 +305,14 @@ export class EmojicoinClient { ...this.getEmojicoinInfo(symbolEmojis), }); const res = this.getTransactionEventData(response); + const { marketNonce, marketID } = res.events.swapEvents[0]; return { ...res, swap: { event: expect(res.events.swapEvents.at(0), Expect.Swap.Event), model: expect(res.models.swapEvents.at(0), Expect.Swap.Model), }, + handle: waitForEventProcessed(marketID, marketNonce, TableName.SwapEvents), }; } @@ -298,12 +331,14 @@ export class EmojicoinClient { ...this.getEmojicoinInfo(symbolEmojis), }); const res = this.getTransactionEventData(response); + const { marketNonce, marketID } = res.events.liquidityEvents[0]; return { ...res, liquidity: { event: expect(res.events.liquidityEvents.at(0), Expect.Liquidity.Event), model: expect(res.models.liquidityEvents.at(0), Expect.Liquidity.Model), }, + handle: waitForEventProcessed(marketID, marketNonce, TableName.LiquidityEvents), }; } @@ -322,12 +357,14 @@ export class EmojicoinClient { ...this.getEmojicoinInfo(symbolEmojis), }); const res = this.getTransactionEventData(response); + const { marketNonce, marketID } = res.events.liquidityEvents[0]; return { ...res, liquidity: { event: expect(res.events.liquidityEvents.at(0), Expect.Liquidity.Event), model: expect(res.models.liquidityEvents.at(0), Expect.Liquidity.Model), }, + handle: waitForEventProcessed(marketID, marketNonce, TableName.LiquidityEvents), }; } diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/home.ts b/src/typescript/sdk/src/indexer-v2/queries/app/home.ts index bbf4cddc8..71df6f297 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/app/home.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/app/home.ts @@ -1,4 +1,6 @@ -import "server-only"; +if (process.env.NODE_ENV !== "test") { + require("server-only"); +} import { LIMIT, ORDER_BY } from "../../../queries/const"; import { SortMarketsBy, type MarketStateQueryArgs } from "../../types/common"; diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/market.ts b/src/typescript/sdk/src/indexer-v2/queries/app/market.ts index 81ed8bb54..bf079dfe2 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/app/market.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/app/market.ts @@ -1,4 +1,6 @@ -import "server-only"; +if (process.env.NODE_ENV !== "test") { + require("server-only"); +} import { LIMIT, ORDER_BY } from "../../../queries"; import { type AnyNumberString } from "../../../types"; diff --git a/src/typescript/sdk/src/indexer-v2/queries/client.ts b/src/typescript/sdk/src/indexer-v2/queries/client.ts index 32b6e3a95..22c768f3d 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/client.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/client.ts @@ -1,4 +1,6 @@ -import "server-only"; +if (process.env.NODE_ENV !== "test") { + require("server-only"); +} import { PostgrestClient } from "@supabase/postgrest-js"; import { parseJSONWithBigInts, stringifyJSONWithBigInts } from "../json-bigint"; diff --git a/src/typescript/sdk/src/indexer-v2/queries/utils.ts b/src/typescript/sdk/src/indexer-v2/queries/utils.ts index e8358c6a2..3547b6c10 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/utils.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/utils.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import "server-only"; +if (process.env.NODE_ENV !== "test") { + require("server-only"); +} import { type PostgrestSingleResponse, diff --git a/src/typescript/sdk/src/queries/index.ts b/src/typescript/sdk/src/queries/index.ts index 0275b036e..8adf250ea 100644 --- a/src/typescript/sdk/src/queries/index.ts +++ b/src/typescript/sdk/src/queries/index.ts @@ -1,3 +1,5 @@ -import "server-only"; +if (process.env.NODE_ENV !== "test") { + require("server-only"); +} export * from "./const"; diff --git a/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts b/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts index d03c35d1f..3c853df17 100644 --- a/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts +++ b/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts @@ -73,8 +73,10 @@ export class DockerTestHarness { /** * Stops the Docker containers. */ - static async stop() { - await execPromise(`docker compose -f ${LOCAL_COMPOSE_PATH} stop`); + static async stop({ frontend }: { frontend: boolean }) { + await execPromise( + `docker compose -f ${LOCAL_COMPOSE_PATH} ${frontend ? "--profile frontend" : ""} stop` + ); const process = Number(readFileSync(TMP_PID_FILE_PATH, { encoding: "utf-8" })); if (process) { kill(process); @@ -84,8 +86,14 @@ export class DockerTestHarness { /** * Calls the Docker helper script to start the containers. */ - static async run(frontend: boolean, filterLogsFrom: ContainerName[] = []) { - await DockerTestHarness.start(frontend, filterLogsFrom); + static async run({ + frontend, + filterLogsFrom = [], + }: { + frontend: boolean; + filterLogsFrom?: ContainerName[]; + }) { + await DockerTestHarness.start({ frontend, filterLogsFrom }); const promises = [ DockerTestHarness.waitForPrimaryService(frontend), DockerTestHarness.waitForDeployer(), @@ -97,7 +105,13 @@ export class DockerTestHarness { /** * Starts a completely new Docker environment for the test harness. */ - static async start(frontend: boolean, filterLogsFrom: ContainerName[]) { + static async start({ + frontend, + filterLogsFrom, + }: { + frontend: boolean; + filterLogsFrom: ContainerName[]; + }) { // Ensure that we have a fresh Docker environment before starting the test harness. await DockerTestHarness.remove(); diff --git a/src/typescript/sdk/tests/post-test.ts b/src/typescript/sdk/tests/post-test.ts index 4e6c8ed73..4e0a386be 100644 --- a/src/typescript/sdk/tests/post-test.ts +++ b/src/typescript/sdk/tests/post-test.ts @@ -2,5 +2,5 @@ import { DockerTestHarness } from "../src/utils/test/docker/docker-test-harness"; export default async function postTest() { - await DockerTestHarness.stop(); + await DockerTestHarness.stop({ frontend: false }); } diff --git a/src/typescript/sdk/tests/pre-test.ts b/src/typescript/sdk/tests/pre-test.ts index c5aae8c6b..3563d6171 100644 --- a/src/typescript/sdk/tests/pre-test.ts +++ b/src/typescript/sdk/tests/pre-test.ts @@ -23,7 +23,7 @@ export default async function preTest() { // Start the docker containers. // -------------------------------------------------------------------------------------- // Start the Docker test harness without the frontend container. - await DockerTestHarness.run(false); + await DockerTestHarness.run({ frontend: false }); // The docker container start-up script publishes the package on-chain. } From 48c1c441d47e5758f0261aba4eeefa8b6974a01a Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Wed, 30 Oct 2024 12:18:42 +0100 Subject: [PATCH 03/94] [ECO-2153] Add sorting e2e tests (#310) --- .../emoji-picker/EmojiPickerWithInput.tsx | 1 + .../components/emoji-table/ClientGrid.tsx | 1 + .../frontend/tests/e2e/market-order.spec.ts | 84 +++++++++++++++++++ .../frontend/tests/e2e/search.spec.ts | 31 ++++++- 4 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/typescript/frontend/tests/e2e/market-order.spec.ts diff --git a/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx b/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx index 136c373ac..8eea83948 100644 --- a/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx +++ b/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx @@ -316,6 +316,7 @@ export const EmojiPickerWithInput = ({ > ctrls.start(e, { snapToCursor: false })} filterEmojis={filterEmojis} diff --git a/src/typescript/frontend/src/components/pages/home/components/emoji-table/ClientGrid.tsx b/src/typescript/frontend/src/components/pages/home/components/emoji-table/ClientGrid.tsx index b9a18c262..cd0eb6b9a 100644 --- a/src/typescript/frontend/src/components/pages/home/components/emoji-table/ClientGrid.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/emoji-table/ClientGrid.tsx @@ -53,6 +53,7 @@ export const ClientGrid = ({ prevIndex={i} runInitialAnimation={true} sortBy={sortBy} + data-testid="market-grid-item" /> ); })} diff --git a/src/typescript/frontend/tests/e2e/market-order.spec.ts b/src/typescript/frontend/tests/e2e/market-order.spec.ts new file mode 100644 index 000000000..687801dbc --- /dev/null +++ b/src/typescript/frontend/tests/e2e/market-order.spec.ts @@ -0,0 +1,84 @@ +import { test, expect } from "@playwright/test"; +import { EmojicoinClient } from "../../../sdk/src/client/emojicoin-client"; +import { getFundedAccount } from "../../../sdk/src/utils/test/test-accounts"; +import { ONE_APT_BIGINT, sleep, SYMBOL_EMOJI_DATA } from "../../../sdk/src"; + +test("check sorting order", async ({ page }) => { + const user = getFundedAccount("777"); + const rat = SYMBOL_EMOJI_DATA.byName("rat")!.emoji; + const emojis = ["cat", "dog", "eagle", "sauropod"]; + const markets = emojis.map(e => [rat, SYMBOL_EMOJI_DATA.byName(e)!.emoji]) + + const client = new EmojicoinClient(); + + // Register markets. + // They all start with rat to simplify the search. + for (let i = 0; i < markets.length; i++) { + await client.register(user, markets[i]).then(res => res.handle); + const amount = 1n * ONE_APT_BIGINT / 100n * BigInt(10 ** (markets.length - i)); + await client.buy(user, markets[i], amount).then(res => res.handle); + } + + await page.goto("/home"); + + // Click the search field. + const search = page.getByTestId("emoji-input"); + expect(search).toBeVisible(); + await sleep(2000); + await search.click(); + + // Expect the emoji picker to be visible. + const picker = page.getByTestId("picker"); + expect(picker).toBeVisible(); + + // Search for "rat" in the emoji picker search field. + const emojiSearch = page.getByPlaceholder("Search"); + expect(emojiSearch).toBeVisible(); + await emojiSearch.fill("rat"); + + // Search for the rat market. + await picker.getByLabel(rat).click(); + + // Expect markets.length results, since that's how many we registered. + let marketGridItems = page.getByTestId("market-grid-item"); + expect(marketGridItems).toHaveCount(markets.length); + + // Expect the sorting button to be visible. + const filters = page.getByText(/{Sort/); + expect(filters).toBeVisible(); + + // Click the sorting button. + await filters.click(); + + // Expect the sort by daily volume button to be visible. + const dailyVolume = page.locator('#emoji-grid-header').getByText('24h Volume'); + expect(dailyVolume).toBeVisible(); + + // Sort by daily volume. + await dailyVolume.click(); + + const names = emojis.map(e => `rat,${e}`); + const patterns = names.map(e => new RegExp(e)); + + // Expect the markets to be in order of daily volume. + marketGridItems = page.locator("#emoji-grid a").getByTitle(/RAT,/, { exact: true }); + expect(marketGridItems).toHaveText(patterns); + + // Click the sorting button. + await filters.click(); + + // Expect the sort by bump order button to be visible. + const bumpOrder = page.locator('#emoji-grid-header').getByText('Bump Order'); + expect(bumpOrder).toBeVisible(); + + // Sort by bump order. + await bumpOrder.click(); + + await page.screenshot(); + + // Expect the markets to be in bump order. + marketGridItems = page.locator("#emoji-grid a").getByTitle(/RAT,/, { exact: true }); + expect(marketGridItems).toHaveText(patterns.reverse()); + + await page.screenshot(); +}); diff --git a/src/typescript/frontend/tests/e2e/search.spec.ts b/src/typescript/frontend/tests/e2e/search.spec.ts index 0ea3b782d..5324b475a 100644 --- a/src/typescript/frontend/tests/e2e/search.spec.ts +++ b/src/typescript/frontend/tests/e2e/search.spec.ts @@ -1,23 +1,48 @@ import { test, expect } from "@playwright/test"; import { EmojicoinClient } from "../../../sdk/src/client/emojicoin-client"; import { getFundedAccount } from "../../../sdk/src/utils/test/test-accounts"; -import { SYMBOL_EMOJI_DATA } from "../../../sdk/src"; +import { sleep, SYMBOL_EMOJI_DATA } from "../../../sdk/src"; test("check search results", async ({ page }) => { const user = getFundedAccount("666"); - const symbols = [SYMBOL_EMOJI_DATA.byName("cat")!.emoji, SYMBOL_EMOJI_DATA.byName("cat")!.emoji]; + const cat = SYMBOL_EMOJI_DATA.byName("cat")!.emoji; + const symbols = [cat,cat]; + const client = new EmojicoinClient(); await client.register(user, symbols).then((res) => res.handle); await page.goto("/home"); + // Click the search field. const search = page.getByTestId("emoji-input"); expect(search).toBeVisible(); - await search.fill(symbols.join("")); + await search.click(); + + // Expect the emoji picker to be visible. + const picker = page.getByTestId("picker"); + expect(picker).toBeVisible(); + + // Search for "cat" in the emoji picker search field. + const emojiSearch = page.getByPlaceholder("Search"); + expect(emojiSearch).toBeVisible(); + await emojiSearch.fill("cat"); + + // Expect the "cat" emoji to be visible in the search results. + let emojiSearchCatButton = picker.getByLabel(cat).first(); + expect(emojiSearchCatButton).toBeVisible(); + + // Search for the cat,cat market. + await emojiSearchCatButton.click({force: true}); + + emojiSearchCatButton = picker.getByLabel(cat).first(); + expect(emojiSearchCatButton).toBeVisible(); + await emojiSearchCatButton.click({force: true}); + // Click on the cat,cat market. const marketCard = page.getByText("cat,cat", { exact: true }); expect(marketCard).toBeVisible(); await marketCard.click(); + // Expect to be redirected to the cat,cat market. await expect(page).toHaveURL(/.*cat;cat/); }); From 4f725b9ab7111a7ab6e1c6bc3457b60c6d9aa631 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Thu, 31 Oct 2024 04:41:07 +0100 Subject: [PATCH 04/94] [ECO-2313] Fix input fields on market and pools pages (#308) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- .../components/inputs/input-numeric/index.tsx | 88 ++++++++++----- .../trade-emojicoin/SwapComponent.tsx | 103 +++++++----------- .../pools/components/liquidity/index.tsx | 101 +++++++++-------- .../frontend/src/lib/utils/decimals.ts | 27 ++--- .../frontend/tests/e2e/market-order.spec.ts | 16 +-- .../frontend/tests/e2e/search.spec.ts | 6 +- src/typescript/sdk/src/utils/index.ts | 1 + src/typescript/sdk/src/utils/validation.ts | 62 +++++++++++ .../sdk/tests/unit/validation.test.ts | 84 ++++++++++++++ 9 files changed, 313 insertions(+), 175 deletions(-) create mode 100644 src/typescript/sdk/src/utils/validation.ts create mode 100644 src/typescript/sdk/tests/unit/validation.test.ts diff --git a/src/typescript/frontend/src/components/inputs/input-numeric/index.tsx b/src/typescript/frontend/src/components/inputs/input-numeric/index.tsx index db0f627c2..9b862db8a 100644 --- a/src/typescript/frontend/src/components/inputs/input-numeric/index.tsx +++ b/src/typescript/frontend/src/components/inputs/input-numeric/index.tsx @@ -1,44 +1,72 @@ -import React from "react"; -import { Input } from "components/inputs/input"; -import { type InputProps } from "components/inputs/input/types"; +import Big from "big.js"; +import React, { useEffect, useState } from "react"; +import { isNumberInConstruction, countDigitsAfterDecimal, sanitizeNumber } from "@sdk/utils"; -const NUMBERS = new Set("0123456789"); +const intToStr = (value: bigint, decimals?: number) => + (Number(value) / 10 ** (decimals ?? 0)).toString(); -export const InputNumeric = ({ +const strToInt = (value: string, decimals?: number) => { + if (isNaN(parseFloat(value))) { + return 0n; + } + const res = Big(value.toString()).mul(Big(10 ** (decimals ?? 0))); + if (res < Big(1)) { + return 0n; + } + return BigInt(res.toString()); +}; + +export const InputNumeric = ({ onUserInput, + decimals, + value, + onSubmit, ...props -}: InputProps & { onUserInput: (value: string) => void }): JSX.Element => { - const [input, setInput] = React.useState(""); - - const onChangeText = (event: React.ChangeEvent) => { - const value = event.target.value.replace(/,/g, ".").replace(/^0+/, "0"); - - let hasDecimal = false; - let s = ""; - for (const char of value) { - if (char === ".") { - if (!hasDecimal) { - s += char; - } else { - hasDecimal = true; - } - } else if (NUMBERS.has(char)) { - s += char; - } +}: { + className?: string; + onUserInput?: (value: bigint) => void; + onSubmit?: (value: bigint) => void; + decimals?: number; + disabled?: boolean; + value: bigint; +}) => { + const [input, setInput] = useState(intToStr(value, decimals)); + + useEffect(() => { + if (strToInt(input, decimals) != value) { + setInput(intToStr(value, decimals)); + } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [value, decimals]); + + const onChangeText = (e: React.ChangeEvent) => { + const value = sanitizeNumber(e.target.value); + + if (!isNumberInConstruction(value)) { + return; } - if (s === "" || !isNaN(Number(s))) { - setInput(s); - onUserInput(s); + const decimalsInValue = countDigitsAfterDecimal(value); + if (typeof decimals === "number" && decimalsInValue > decimals) { + return; + } + + setInput(value); + if (onUserInput) { + onUserInput(strToInt(value, decimals)); } }; return ( - onChangeText(event)} + onChangeText(e)} value={input} + onKeyDown={(e) => { + if (e.key === "Enter" && onSubmit) { + onSubmit(strToInt(input, decimals)); + } + }} {...props} /> ); diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx index 65482e972..66d42b186 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx @@ -5,7 +5,6 @@ import { type PropsWithChildren, useEffect, useState, - useCallback, useMemo, type MouseEventHandler, } from "react"; @@ -26,6 +25,7 @@ import { toCoinTypes } from "@sdk/markets/utils"; import { Flex, FlexGap } from "@containers"; import Popup from "components/popup"; import { Text } from "components/text"; +import { InputNumeric } from "components/inputs"; const SmallButton = ({ emoji, @@ -79,8 +79,7 @@ const inputAndOutputStyles = ` border-transparent !p-0 text-white `; -const APT_DISPLAY_DECIMALS = 4; -const EMOJICOIN_DISPLAY_DECIMALS = 1; +const OUTPUT_DISPLAY_DECIMALS = 4; const SWAP_GAS_COST = 52500n; export default function SwapComponent({ @@ -103,7 +102,7 @@ export default function SwapComponent({ const [inputAmount, setInputAmount] = useState( toActualCoinDecimals({ num: presetInputAmountIsValid ? presetInputAmount! : "1" }) ); - const [outputAmount, setOutputAmount] = useState("0"); + const [outputAmount, setOutputAmount] = useState(0n); const [previous, setPrevious] = useState(inputAmount); const [isLoading, setIsLoading] = useState(false); const [isSell, setIsSell] = useState(!(searchParams.get("sell") === null)); @@ -125,15 +124,18 @@ export default function SwapComponent({ const swapResult = useSimulateSwap({ marketAddress, - inputAmount: inputAmount === "" ? "0" : inputAmount, + inputAmount: inputAmount.toString(), isSell, numSwaps, }); + const outputAmountString = toDisplayCoinDecimals({ + num: isLoading ? previous : outputAmount, + decimals: OUTPUT_DISPLAY_DECIMALS, + }); + const { ref, replay } = useScramble({ - text: Number(isLoading ? previous : outputAmount).toFixed( - isSell ? APT_DISPLAY_DECIMALS : EMOJICOIN_DISPLAY_DECIMALS - ), + text: new Intl.NumberFormat().format(Number(outputAmountString)), overdrive: false, overflow: true, speed: isLoading ? 0.4 : 1000, @@ -151,52 +153,19 @@ export default function SwapComponent({ setIsLoading(true); return; } - const swapResultDisplay = toDisplayNumber(swapResult, isSell ? "apt" : "emoji"); - setPrevious(swapResultDisplay); - setOutputAmount(swapResultDisplay); + setPrevious(swapResult); + setOutputAmount(swapResult); setIsLoading(false); replay(); }, [swapResult, replay, isSell]); - const toDisplayNumber = (value: bigint | number | string, type: "apt" | "emoji" = "apt") => { - const badString = typeof value === "string" && (value === "" || isNaN(parseInt(value))); - if (!value || badString) { - return "0"; - } - // We use the APT display decimal amount here to avoid early truncation. - return toDisplayCoinDecimals({ - num: value, - decimals: type === "apt" ? APT_DISPLAY_DECIMALS : EMOJICOIN_DISPLAY_DECIMALS, - }).toString(); - }; - - const handleInput = (e: React.ChangeEvent) => { - if (e.target.value === "") { - setInputAmount(""); - } - if (isNaN(parseFloat(e.target.value))) { - e.stopPropagation(); - return; - } - setInputAmount(toActualCoinDecimals({ num: e.target.value })); - }; - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "Enter" && submit) { - submit(); - } - }, - [submit] - ); - const sufficientBalance = useMemo(() => { if (!account || (isSell && !emojicoinBalance) || (!isSell && !aptBalance)) return false; if (account) { if (isSell) { - return emojicoinBalance >= BigInt(inputAmount); + return emojicoinBalance >= inputAmount; } - return aptBalance >= BigInt(inputAmount); + return aptBalance >= inputAmount; } }, [account, aptBalance, emojicoinBalance, isSell, inputAmount]); @@ -205,7 +174,7 @@ export default function SwapComponent({ const coinBalance = isSell ? emojicoinBalance : aptBalance; const balance = toDisplayCoinDecimals({ num: coinBalance, - decimals: !isSell ? APT_DISPLAY_DECIMALS : EMOJICOIN_DISPLAY_DECIMALS, + decimals: 4, }); return ( @@ -235,12 +204,16 @@ export default function SwapComponent({ setInputAmount(String(emojicoinBalance / 2n))} + onClick={() => { + setInputAmount(emojicoinBalance / 2n); + }} /> setInputAmount(String(emojicoinBalance))} + onClick={() => { + setInputAmount(emojicoinBalance); + }} /> ) : ( @@ -248,17 +221,23 @@ export default function SwapComponent({ setInputAmount(String(availableAptBalance / 4n))} + onClick={() => { + setInputAmount(availableAptBalance / 4n); + }} /> setInputAmount(String(availableAptBalance / 2n))} + onClick={() => { + setInputAmount(availableAptBalance / 2n); + }} /> setInputAmount(String(availableAptBalance))} + onClick={() => { + setInputAmount(availableAptBalance); + }} /> )} @@ -272,25 +251,23 @@ export default function SwapComponent({ {isSell ? t("You sell") : t("You pay")} {balanceLabel} - + value={inputAmount} + onUserInput={(v) => setInputAmount(v)} + onSubmit={() => (submit ? submit() : {})} + decimals={8} + /> {isSell ? : } { - setInputAmount(toActualCoinDecimals({ num: outputAmount })); + setInputAmount(outputAmount); + // This is done as to not display an old value if the swap simulation fails. + setOutputAmount(0n); + setPrevious(0n); setIsSell((v) => !v); }} /> diff --git a/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx b/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx index 39655b42c..84ef50b4b 100644 --- a/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx +++ b/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx @@ -4,7 +4,7 @@ import React, { type PropsWithChildren, useEffect, useMemo, useState } from "rea import { useThemeContext } from "context"; import { translationFunction } from "context/language-context"; import { Flex, Column, FlexGap } from "@containers"; -import { Text, Button } from "components"; +import { Text, Button, InputNumeric } from "components"; import { StyledAddLiquidityWrapper } from "./styled"; import { ProvideLiquidity, RemoveLiquidity } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun"; import { toCoinDecimalString } from "lib/utils/decimals"; @@ -41,15 +41,6 @@ const fmtCoin = (n: AnyNumberString | undefined) => { return new Intl.NumberFormat().format(Number(toCoinDecimalString(n, 8))); }; -const unfmtCoin = (n: AnyNumberString) => { - return BigInt( - toActualCoinDecimals({ - num: typeof n === "bigint" ? n : Number(n), - decimals: 0, - }) - ); -}; - const InnerWrapper = ({ children, id, @@ -82,18 +73,27 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { const { theme } = useThemeContext(); const searchParams = useSearchParams(); + const presetInputAmount = searchParams.get("add") !== null ? searchParams.get("add") : searchParams.get("remove"); const presetInputAmountIsValid = presetInputAmount !== null && presetInputAmount !== "" && !Number.isNaN(Number(presetInputAmount)); - const [liquidity, setLiquidity] = useState( - searchParams.get("add") !== null && presetInputAmountIsValid ? Number(presetInputAmount) : "" + + const [liquidity, setLiquidity] = useState( + toActualCoinDecimals({ + num: searchParams.get("add") !== null && presetInputAmountIsValid ? presetInputAmount! : "1", + }) ); - const [lp, setLP] = useState( - searchParams.get("remove") !== null && presetInputAmountIsValid ? Number(presetInputAmount) : "" + + const [lp, setLP] = useState( + toActualCoinDecimals({ + num: + searchParams.get("remove") !== null && presetInputAmountIsValid ? presetInputAmount! : "1", + }) ); + const [direction, setDirection] = useState<"add" | "remove">( searchParams.get("remove") !== null ? "remove" : "add" ); @@ -113,21 +113,19 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { const provideLiquidityResult = useSimulateProvideLiquidity({ marketAddress: market?.market.marketAddress, - quoteAmount: unfmtCoin(liquidity ?? 0), + quoteAmount: liquidity ?? 0, }); const { emojicoin } = market ? toCoinTypes(market?.market.marketAddress) : { emojicoin: "" }; const removeLiquidityResult = useSimulateRemoveLiquidity({ marketAddress: market?.market.marketAddress, - lpCoinAmount: unfmtCoin(lp ?? 0), + lpCoinAmount: lp ?? 0, typeTags: [emojicoin ?? ""], }); const enoughApt = - direction === "add" - ? aptBalance !== undefined && aptBalance >= unfmtCoin(liquidity ?? 0) - : true; + direction === "add" ? aptBalance !== undefined && aptBalance >= (liquidity ?? 0) : true; const enoughEmoji = direction === "add" ? emojicoinBalance !== undefined && @@ -135,7 +133,7 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { : true; const enoughEmojiLP = direction === "remove" - ? emojicoinLPBalance !== undefined && emojicoinLPBalance >= unfmtCoin(lp ?? 0) + ? emojicoinLPBalance !== undefined && emojicoinLPBalance >= (lp ?? 0) : true; useEffect(() => { @@ -157,7 +155,7 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { const isActionPossible = market !== undefined && - (direction === "add" ? liquidity !== "" : lp !== "") && + (direction === "add" ? liquidity !== 0n : lp !== 0n) && enoughApt && enoughEmoji && enoughEmojiLP; @@ -175,22 +173,20 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { {fmtCoin(aptBalance)} {")"} - setLiquidity(e.target.value === "" ? "" : Number(e.target.value))} - style={{ - color: direction === "remove" ? theme.colors.lightGray + "99" : "white", - }} - min={0} - step={0.01} - type={direction === "add" ? "number" : "text"} - disabled={direction === "remove"} - value={ - direction === "add" - ? liquidity - : (fmtCoin(removeLiquidityResult?.quote_amount) ?? "...") - } - > + {direction === "add" ? ( + setLiquidity(e)} + value={liquidity} + decimals={8} + /> + ) : ( + + )} @@ -217,7 +213,6 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { ? (fmtCoin(provideLiquidityResult?.base_amount) ?? "...") : (fmtCoin(removeLiquidityResult?.base_amount) ?? "...") } - type="text" disabled > @@ -236,18 +231,20 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { {")"} - setLP(e.target.value === "" ? "" : Number(e.target.value))} - disabled={direction === "add"} - > + {direction === "add" ? ( + + ) : ( + setLP(e)} + value={lp} + decimals={8} + /> + )} @@ -318,7 +315,7 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { aptosConfig: aptos.config, provider: account.address, marketAddress: market!.market.marketAddress, - quoteAmount: unfmtCoin(liquidity ?? 0), + quoteAmount: liquidity ?? 0, typeTags: [emojicoin, emojicoinLP], minLpCoinsOut: 1n, }); @@ -328,7 +325,7 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { aptosConfig: aptos.config, provider: account.address, marketAddress: market!.market.marketAddress, - lpCoinAmount: unfmtCoin(lp), + lpCoinAmount: lp, typeTags: [emojicoin, emojicoinLP], minQuoteOut: 1n, }); diff --git a/src/typescript/frontend/src/lib/utils/decimals.ts b/src/typescript/frontend/src/lib/utils/decimals.ts index 3ed1bb4f0..341f428bc 100644 --- a/src/typescript/frontend/src/lib/utils/decimals.ts +++ b/src/typescript/frontend/src/lib/utils/decimals.ts @@ -20,26 +20,12 @@ export const toCoinDecimalString = (num: AnyNumberString, displayDecimals?: numb * @example * 1 APT => 100000000 */ -const toActualCoinDecimals = ({ - num, - round, - decimals, -}: { - num: AnyNumberString; - round?: number; - decimals?: number; -}): string => { +const toActualCoinDecimals = ({ num }: { num: AnyNumberString }): bigint => { if (typeof num === "string" && isNaN(parseFloat(num))) { - return "0"; - } - let res = Big(num.toString()).mul(Big(10 ** DECIMALS)); - if (typeof round !== "undefined") { - res = res.round(round); + return 0n; } - if (typeof decimals !== "undefined") { - return res.toFixed(decimals).toString(); - } - return res.toString(); + const res = Big(num.toString()).mul(Big(10 ** DECIMALS)); + return BigInt(res.toString()); }; /** @@ -68,7 +54,10 @@ const toDisplayCoinDecimals = ({ res = res.round(round); } if (typeof decimals !== "undefined") { - return res.toFixed(decimals).toString(); + if (res < Big(1)) { + return res.toPrecision(decimals).replace(/\.?0+$/, ""); + } + return res.toFixed(decimals).replace(/\.?0+$/, ""); } return res.toString(); }; diff --git a/src/typescript/frontend/tests/e2e/market-order.spec.ts b/src/typescript/frontend/tests/e2e/market-order.spec.ts index 687801dbc..8c00837a0 100644 --- a/src/typescript/frontend/tests/e2e/market-order.spec.ts +++ b/src/typescript/frontend/tests/e2e/market-order.spec.ts @@ -7,16 +7,16 @@ test("check sorting order", async ({ page }) => { const user = getFundedAccount("777"); const rat = SYMBOL_EMOJI_DATA.byName("rat")!.emoji; const emojis = ["cat", "dog", "eagle", "sauropod"]; - const markets = emojis.map(e => [rat, SYMBOL_EMOJI_DATA.byName(e)!.emoji]) + const markets = emojis.map((e) => [rat, SYMBOL_EMOJI_DATA.byName(e)!.emoji]); const client = new EmojicoinClient(); // Register markets. // They all start with rat to simplify the search. for (let i = 0; i < markets.length; i++) { - await client.register(user, markets[i]).then(res => res.handle); - const amount = 1n * ONE_APT_BIGINT / 100n * BigInt(10 ** (markets.length - i)); - await client.buy(user, markets[i], amount).then(res => res.handle); + await client.register(user, markets[i]).then((res) => res.handle); + const amount = ((1n * ONE_APT_BIGINT) / 100n) * BigInt(10 ** (markets.length - i)); + await client.buy(user, markets[i], amount).then((res) => res.handle); } await page.goto("/home"); @@ -51,14 +51,14 @@ test("check sorting order", async ({ page }) => { await filters.click(); // Expect the sort by daily volume button to be visible. - const dailyVolume = page.locator('#emoji-grid-header').getByText('24h Volume'); + const dailyVolume = page.locator("#emoji-grid-header").getByText("24h Volume"); expect(dailyVolume).toBeVisible(); // Sort by daily volume. await dailyVolume.click(); - const names = emojis.map(e => `rat,${e}`); - const patterns = names.map(e => new RegExp(e)); + const names = emojis.map((e) => `rat,${e}`); + const patterns = names.map((e) => new RegExp(e)); // Expect the markets to be in order of daily volume. marketGridItems = page.locator("#emoji-grid a").getByTitle(/RAT,/, { exact: true }); @@ -68,7 +68,7 @@ test("check sorting order", async ({ page }) => { await filters.click(); // Expect the sort by bump order button to be visible. - const bumpOrder = page.locator('#emoji-grid-header').getByText('Bump Order'); + const bumpOrder = page.locator("#emoji-grid-header").getByText("Bump Order"); expect(bumpOrder).toBeVisible(); // Sort by bump order. diff --git a/src/typescript/frontend/tests/e2e/search.spec.ts b/src/typescript/frontend/tests/e2e/search.spec.ts index 5324b475a..bac4615dd 100644 --- a/src/typescript/frontend/tests/e2e/search.spec.ts +++ b/src/typescript/frontend/tests/e2e/search.spec.ts @@ -6,7 +6,7 @@ import { sleep, SYMBOL_EMOJI_DATA } from "../../../sdk/src"; test("check search results", async ({ page }) => { const user = getFundedAccount("666"); const cat = SYMBOL_EMOJI_DATA.byName("cat")!.emoji; - const symbols = [cat,cat]; + const symbols = [cat, cat]; const client = new EmojicoinClient(); await client.register(user, symbols).then((res) => res.handle); @@ -32,11 +32,11 @@ test("check search results", async ({ page }) => { expect(emojiSearchCatButton).toBeVisible(); // Search for the cat,cat market. - await emojiSearchCatButton.click({force: true}); + await emojiSearchCatButton.click({ force: true }); emojiSearchCatButton = picker.getByLabel(cat).first(); expect(emojiSearchCatButton).toBeVisible(); - await emojiSearchCatButton.click({force: true}); + await emojiSearchCatButton.click({ force: true }); // Click on the cat,cat market. const marketCard = page.getByText("cat,cat", { exact: true }); diff --git a/src/typescript/sdk/src/utils/index.ts b/src/typescript/sdk/src/utils/index.ts index 9807ed61d..b3ec761ed 100644 --- a/src/typescript/sdk/src/utils/index.ts +++ b/src/typescript/sdk/src/utils/index.ts @@ -3,3 +3,4 @@ export * from "./hex"; export * from "./misc"; export * from "./type-tags"; export * from "./compare-bigint"; +export * from "./validation"; diff --git a/src/typescript/sdk/src/utils/validation.ts b/src/typescript/sdk/src/utils/validation.ts new file mode 100644 index 000000000..3bcd0386d --- /dev/null +++ b/src/typescript/sdk/src/utils/validation.ts @@ -0,0 +1,62 @@ +/** + * Removes all useless leading zeros. + * In the case of 0.[0-9]+, the leading zero will not be removed. + * @param {string} input - The string to process + * @returns {string} The processed string + */ +export const trimLeadingZeros = (input: string) => { + // Replace all leading zeros with one zero + input = input.replace(/^0+/, "0"); // The regex matches all leading 0s. + + if (input.startsWith("0") && !input.startsWith("0.") && input.length > 1) { + input = input.slice(1); + } + + return input; +}; + +/** + * Removes all leading zeros and transforms "," into ".". + * .[0-9]+ will be replaced with 0.[0-9]+ + * @param {string} input - The string to process + * @returns {string} The processed string + */ +export const sanitizeNumber = (input: string) => { + input = trimLeadingZeros(input.replace(/,/, ".")); + if (input.startsWith(".")) { + return `0${input}`; + } + return input; +}; + +/** + * Checks if the input is a number in construction. + * + * This facilitates using temporarily invalid numbers that will eventually be valid- aka, they are + * numbers in construction. + * + * For example, to input 0.001, you need to input "0", then "0." (invalid), + * which should be allowed to reach "0.001". + * + * Valid examples: + * - 0.1 + * - 0 + * - 0.000 + * - .0 + * - 0. + * + * Invalid examples: + * - 0.0.1 + * + * @param {string} input - The string to test + * @returns {boolean} True if the input is a number in construction + */ +export const isNumberInConstruction = (input: string) => /^[0-9]*(\.([0-9]*)?)?$/.test(input); + +/** + * Counts the number of digits after the decimal point in a string number. + * @param {string} input - The numeric string to analyze (e.g., "123.456") + * @returns {number} The count of digits after the decimal point (e.g., "123.456" returns 3) + */ +export const countDigitsAfterDecimal = (input: string) => + /\./.test(input) ? input.split(".")[1].length : 0; diff --git a/src/typescript/sdk/tests/unit/validation.test.ts b/src/typescript/sdk/tests/unit/validation.test.ts new file mode 100644 index 000000000..bc14f1666 --- /dev/null +++ b/src/typescript/sdk/tests/unit/validation.test.ts @@ -0,0 +1,84 @@ +import * as validation from "../../src/utils/validation"; + +describe("validation utility functions", () => { + it("should trim leading zeros", () => { + const givenAndExpected = [ + ["00", "0"], + ["00000000", "0"], + ["01", "1"], + ["001", "1"], + ["000000001", "1"], + ["00.1", "0.1"], + ["00000000.1", "0.1"], + ["01.1", "1.1"], + ["001.1", "1.1"], + ["000000001.1", "1.1"], + ["0.1", "0.1"], + ["10", "10"], + ["100", "100"], + ]; + + givenAndExpected.forEach(([given, expected]) => { + expect(validation.trimLeadingZeros(given)).toEqual(expected); + }); + }); + + it("should sanitize number", () => { + const givenAndExpected = [ + [",1", "0.1"], + [",0", "0.0"], + ["0,1", "0.1"], + ["0,0", "0.0"], + [",100", "0.100"], + [",000", "0.000"], + ["0000,0", "0.0"], + ["0000,001", "0.001"], + ]; + + givenAndExpected.forEach(([given, expected]) => { + expect(validation.sanitizeNumber(given)).toEqual(expected); + }); + }); + + it("should accurately test if number is in construction", () => { + const givenAndExpected: [string, boolean][] = [ + [".0", true], + [".1", true], + ["0.0", true], + ["0.1", true], + ["1.0", true], + ["1.1", true], + ["0", true], + ["0.000", true], + ["0.", true], + ["1.", true], + ["0.0.1", false], + ["0.abc", false], + ["abc.0", false], + ]; + + givenAndExpected.forEach(([given, expected]) => { + expect(validation.isNumberInConstruction(given)).toEqual(expected); + }); + }); + + it("should accurately return the number of decimals", () => { + const givenAndExpected: [string, number][] = [ + ["0", 0], + ["0.", 0], + [".0", 1], + ["0.0", 1], + ["0.00", 2], + ["00.00", 2], + ["000.00", 2], + ["0.0001", 4], + [".0001", 4], + ["54423536.23466245", 8], + [".23466245", 8], + ]; + + givenAndExpected.forEach(([given, expected]) => { + expect(validation.countDigitsAfterDecimal(given)).toEqual(expected); + }); + }); +}); From 9cd7b8138c35fef9c348f16e70a85dd619fe388e Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:53:49 -0700 Subject: [PATCH 05/94] [ECO-2361] Add test page `/verify_status` for checking Galxe campaign, allowlisted, and geoblocked status (#318) --- .../frontend/src/app/verify_status/page.tsx | 12 +++ .../pages/verify_status/VerifyStatusPage.tsx | 89 +++++++++++++++++++ .../verify_status/get-verification-status.ts | 14 +++ .../frontend/src/lib/utils/allowlist.ts | 34 ++++--- src/typescript/frontend/src/middleware.ts | 3 +- 5 files changed, 138 insertions(+), 14 deletions(-) create mode 100644 src/typescript/frontend/src/app/verify_status/page.tsx create mode 100644 src/typescript/frontend/src/components/pages/verify_status/VerifyStatusPage.tsx create mode 100644 src/typescript/frontend/src/components/pages/verify_status/get-verification-status.ts diff --git a/src/typescript/frontend/src/app/verify_status/page.tsx b/src/typescript/frontend/src/app/verify_status/page.tsx new file mode 100644 index 000000000..71dabc57e --- /dev/null +++ b/src/typescript/frontend/src/app/verify_status/page.tsx @@ -0,0 +1,12 @@ +import VerifyStatusPage from "components/pages/verify_status/VerifyStatusPage"; +import { headers } from "next/headers"; +import { isUserGeoblocked } from "utils/geolocation"; + +export const dynamic = "force-dynamic"; + +const Verify = async () => { + const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); + return ; +}; + +export default Verify; diff --git a/src/typescript/frontend/src/components/pages/verify_status/VerifyStatusPage.tsx b/src/typescript/frontend/src/components/pages/verify_status/VerifyStatusPage.tsx new file mode 100644 index 000000000..fe2ed0849 --- /dev/null +++ b/src/typescript/frontend/src/components/pages/verify_status/VerifyStatusPage.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useWallet } from "@aptos-labs/wallet-adapter-react"; +import ButtonWithConnectWalletFallback from "components/header/wallet-button/ConnectWalletButton"; +import { useAptos } from "context/wallet-context/AptosContextProvider"; +import { useEffect, useState } from "react"; +import { motion } from "framer-motion"; +import { standardizeAddress, truncateAddress } from "@sdk/utils"; +import { getVerificationStatus } from "./get-verification-status"; +import { EXTERNAL_LINK_PROPS } from "components/link"; + +const checkmarkOrX = (bool: boolean) => { + return {bool ? "✅" : "❌"} ; +}; + +export const ClientVerifyPage: React.FC<{ geoblocked: boolean }> = ({ geoblocked }) => { + const { account } = useAptos(); + const { connected, disconnect } = useWallet(); + const [galxe, setGalxe] = useState(false); + const [customAllowlisted, setCustomAllowlisted] = useState(false); + + useEffect(() => { + if (!account || !connected) { + setGalxe(false); + setCustomAllowlisted(false); + } else { + const address = standardizeAddress(account.address); + getVerificationStatus(address).then(({ galxe, customAllowlisted }) => { + setGalxe(galxe); + setCustomAllowlisted(customAllowlisted); + }); + } + }, [account, connected]); + + return ( + <> +
+
+
+ {connected && ( + { + disconnect(); + }} + transition={{ + type: "just", + duration: 0.3, + }} + > + {"<<"}  + Disconnect Wallet + + )} + +
+
+ Wallet address:{" "} + + {account && {`0x${truncateAddress(account.address).substring(2)}`}} + +
+
Galxe: {checkmarkOrX(galxe)}
+
Custom allowlist: {checkmarkOrX(customAllowlisted)}
+
Passes geoblocking: {checkmarkOrX(geoblocked)}
+ + Galxe Campaign + +
+
+
+
+
+ + ); +}; + +export default ClientVerifyPage; diff --git a/src/typescript/frontend/src/components/pages/verify_status/get-verification-status.ts b/src/typescript/frontend/src/components/pages/verify_status/get-verification-status.ts new file mode 100644 index 000000000..085f83253 --- /dev/null +++ b/src/typescript/frontend/src/components/pages/verify_status/get-verification-status.ts @@ -0,0 +1,14 @@ +"use server"; + +import { isInGalxeCampaign, isOnCustomAllowlist } from "lib/utils/allowlist"; + +export async function getVerificationStatus(address: `0x${string}`) { + const [galxe, customAllowlisted] = await Promise.all([ + isInGalxeCampaign(address), + isOnCustomAllowlist(address), + ]); + return { + galxe, + customAllowlisted, + }; +} diff --git a/src/typescript/frontend/src/lib/utils/allowlist.ts b/src/typescript/frontend/src/lib/utils/allowlist.ts index 9102950a3..d64380324 100644 --- a/src/typescript/frontend/src/lib/utils/allowlist.ts +++ b/src/typescript/frontend/src/lib/utils/allowlist.ts @@ -10,24 +10,19 @@ export const GALXE_URL = "https://graphigo.prd.galaxy.eco/query"; // If IS_ALLOWLIST_ENABLED is not truthy, the function returns true. // // The address can be provided either as "0xabc" or directly "abc". -export async function isAllowListed(address: string): Promise { +export async function isAllowListed(addressIn: string): Promise { if (!IS_ALLOWLIST_ENABLED) { return true; } - if (!address.startsWith("0x")) { - address = `0x${address}`; - } + const address = addressIn.startsWith("0x") + ? (addressIn as `0x${string}`) + : (`0x${addressIn}` as const); - if (ALLOWLISTER3K_URL !== undefined) { - const condition = await fetch(`${ALLOWLISTER3K_URL}/${address}`) - .then((r) => r.text()) - .then((data) => data === "true"); - if (condition) { - return true; - } - } + return isInGalxeCampaign(address) || isOnCustomAllowlist(address); +} +export const isInGalxeCampaign = async (address: `0x${string}`): Promise => { if (GALXE_CAMPAIGN_ID !== undefined) { const condition = await fetch(GALXE_URL, { method: "POST", @@ -57,4 +52,17 @@ export async function isAllowListed(address: string): Promise { } return false; -} +}; + +export const isOnCustomAllowlist = async (address: `0x${string}`): Promise => { + if (ALLOWLISTER3K_URL !== undefined) { + const condition = await fetch(`${ALLOWLISTER3K_URL}/${address}`) + .then((r) => r.text()) + .then((data) => data === "true"); + if (condition) { + return true; + } + } + + return false; +}; diff --git a/src/typescript/frontend/src/middleware.ts b/src/typescript/frontend/src/middleware.ts index 55350fbb0..8b74781a6 100644 --- a/src/typescript/frontend/src/middleware.ts +++ b/src/typescript/frontend/src/middleware.ts @@ -15,7 +15,8 @@ export default async function middleware(request: NextRequest) { pathname === "/webclip.png" || pathname === "/icon.png" || pathname === "/test" || - pathname === "/geolocation" + pathname === "/geolocation" || + pathname === "/verify_status" ) { return NextResponse.next(); } From b6b762c11cb8bee3be24aa58245f4d65e93d90df Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Fri, 1 Nov 2024 15:09:26 +0100 Subject: [PATCH 06/94] [ECO-2233] Add price feed e2e test (#285) --- cfg/cspell-frontend-dictionary.txt | 2 + src/docker/utils/prune.sh | 4 +- src/typescript/ci.env | 1 + src/typescript/example.env | 4 ++ .../sdk/src/indexer-v2/types/index.ts | 2 +- .../utils/test/docker/docker-test-harness.ts | 2 +- src/typescript/sdk/tests/e2e/helpers/misc.ts | 5 ++ .../e2e/queries/price-feed/price-feed.test.ts | 44 ++++++++++++++++ .../test_1_insert_current_day_swap.sql | 52 +++++++++++++++++++ .../price-feed/test_1_insert_market_state.sql | 44 ++++++++++++++++ .../test_1_insert_past_day_swap.sql | 52 +++++++++++++++++++ .../price-feed/test_2_insert_earlier_swap.sql | 52 +++++++++++++++++++ .../price-feed/test_2_insert_later_swap.sql | 52 +++++++++++++++++++ .../price-feed/test_2_insert_market_state.sql | 44 ++++++++++++++++ 14 files changed, 356 insertions(+), 4 deletions(-) create mode 100644 src/typescript/sdk/tests/e2e/queries/price-feed/price-feed.test.ts create mode 100644 src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_current_day_swap.sql create mode 100644 src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_market_state.sql create mode 100644 src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_past_day_swap.sql create mode 100644 src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_earlier_swap.sql create mode 100644 src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_later_swap.sql create mode 100644 src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_market_state.sql diff --git a/cfg/cspell-frontend-dictionary.txt b/cfg/cspell-frontend-dictionary.txt index 55b0795a2..28023fad9 100644 --- a/cfg/cspell-frontend-dictionary.txt +++ b/cfg/cspell-frontend-dictionary.txt @@ -51,3 +51,5 @@ vpnapi ctrls dockerenv testid +ADBEEF +bytea diff --git a/src/docker/utils/prune.sh b/src/docker/utils/prune.sh index 133a10c59..b09b28a6d 100755 --- a/src/docker/utils/prune.sh +++ b/src/docker/utils/prune.sh @@ -137,7 +137,7 @@ if [ -n "$reset_localnet" ]; then # Bind-mounting the parent of `.aptos` gives the container the right to # delete it. docker run --rm -v "$docker_dir/localnet:/pwd" busybox rm -rf /pwd/.aptos - docker compose -f compose.local.yaml --profile frontend down --volumes + docker compose -f compose.local.yaml --profile frontend --env-file example.local.env down --volumes else - docker compose -f compose.local.yaml --profile frontend down + docker compose -f compose.local.yaml --profile frontend --env-file example.local.env down fi diff --git a/src/typescript/ci.env b/src/typescript/ci.env index 3d3bff668..13a5861e9 100644 --- a/src/typescript/ci.env +++ b/src/typescript/ci.env @@ -11,3 +11,4 @@ EMOJICOIN_INDEXER_URL="http://localhost:3000" REVALIDATION_TIME="1" HASH_SEED="some random string that is not public" PUBLISHER_PRIVATE_KEY="eaa964d1353b075ac63b0c5a0c1e92aa93355be1402f6077581e37e2a846105e" +DB_URL="postgres://emojicoin:emojicoin@localhost/emojicoin" diff --git a/src/typescript/example.env b/src/typescript/example.env index 106358c9a..6b5a6ffae 100644 --- a/src/typescript/example.env +++ b/src/typescript/example.env @@ -65,3 +65,7 @@ VPNAPI_IO_API_KEY="" # No "0x" at the start. # Useful in testing only. PUBLISHER_PRIVATE_KEY="" + +# The URL to connect to the PostgreSQL database. +# Useful in testing only. +DB_URL="" diff --git a/src/typescript/sdk/src/indexer-v2/types/index.ts b/src/typescript/sdk/src/indexer-v2/types/index.ts index 540e1ec6b..fe7a58f56 100644 --- a/src/typescript/sdk/src/indexer-v2/types/index.ts +++ b/src/typescript/sdk/src/indexer-v2/types/index.ts @@ -587,7 +587,7 @@ export const toUserPoolsRPCResponse = (data: DatabaseJsonType["user_pools"]) => const q64ToBigInt = (n: string) => BigInt(Big(n).div(Big(2).pow(64)).toFixed(0)); export const toPriceFeedRPCResponse = (data: DatabaseJsonType["price_feed"]) => ({ - marketID: data.market_id, + marketID: BigInt(data.market_id), symbolBytes: data.symbol_bytes, symbolEmojis: data.symbol_emojis, marketAddress: data.market_address, diff --git a/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts b/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts index 3c853df17..ac4189b31 100644 --- a/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts +++ b/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts @@ -75,7 +75,7 @@ export class DockerTestHarness { */ static async stop({ frontend }: { frontend: boolean }) { await execPromise( - `docker compose -f ${LOCAL_COMPOSE_PATH} ${frontend ? "--profile frontend" : ""} stop` + `docker compose -f ${LOCAL_COMPOSE_PATH} ${frontend ? "--profile frontend" : ""} --env-file ${LOCAL_ENV_PATH} stop` ); const process = Number(readFileSync(TMP_PID_FILE_PATH, { encoding: "utf-8" })); if (process) { diff --git a/src/typescript/sdk/tests/e2e/helpers/misc.ts b/src/typescript/sdk/tests/e2e/helpers/misc.ts index 92432b273..7870c1b89 100644 --- a/src/typescript/sdk/tests/e2e/helpers/misc.ts +++ b/src/typescript/sdk/tests/e2e/helpers/misc.ts @@ -7,6 +7,7 @@ import { rawPeriodToEnum, type Types, } from "../../../src"; +import postgres from "postgres"; export const getTrackerFromWriteSet = ( res: UserTransactionResponse, @@ -41,3 +42,7 @@ export const getOneMinutePeriodicStateEvents = (res: UserTransactionResponse) => getEvents(res).periodicStateEvents.filter( (e) => rawPeriodToEnum(e.periodicStateMetadata.period) === Period.Period1M ); + +export const getDbConnection = () => { + return postgres(process.env.DB_URL!); +}; diff --git a/src/typescript/sdk/tests/e2e/queries/price-feed/price-feed.test.ts b/src/typescript/sdk/tests/e2e/queries/price-feed/price-feed.test.ts new file mode 100644 index 000000000..b9b7bbc5e --- /dev/null +++ b/src/typescript/sdk/tests/e2e/queries/price-feed/price-feed.test.ts @@ -0,0 +1,44 @@ +import { getDbConnection } from "../../helpers"; +import { fetchPriceFeed } from "../../../../src/indexer-v2/queries"; +import path from "path"; + +const pathRoot = path.join(__dirname, "./"); + +describe("queries price_feed and returns accurate price feed data", () => { + it("checks price feed results generated from artificial data", async () => { + const db = getDbConnection(); + + // Insert a swap 25 hours ago at price 500 + await db.file(`${pathRoot}test_1_insert_past_day_swap.sql`); + + // Insert a fresh swap at price 750 + await db.file(`${pathRoot}test_1_insert_current_day_swap.sql`); + + // Update market_latest_state_event accordingly + await db.file(`${pathRoot}test_1_insert_market_state.sql`); + + // Insert a swap 10 hours ago at price 1000 + await db.file(`${pathRoot}test_2_insert_earlier_swap.sql`); + + // Insert a fresh swap at price 250 + await db.file(`${pathRoot}test_2_insert_later_swap.sql`); + + // Update market_latest_state_event accordingly + await db.file(`${pathRoot}test_2_insert_market_state.sql`); + + const priceFeed = await fetchPriceFeed({}); + const market_777701 = priceFeed.find((m) => m.marketID === 777701n); + expect(market_777701).toBeDefined(); + expect(market_777701!.marketID).toEqual(777701n); + expect(market_777701!.openPrice).toEqual(500n); + expect(market_777701!.closePrice).toEqual(750n); + expect(market_777701!.deltaPercentage).toEqual(50); + + const market_777702 = priceFeed.find((m) => m.marketID === 777702n); + expect(market_777702).toBeDefined(); + expect(market_777702!.marketID).toEqual(777702n); + expect(market_777702!.openPrice).toEqual(1000n); + expect(market_777702!.closePrice).toEqual(250n); + expect(market_777702!.deltaPercentage).toEqual(-75); + }); +}); diff --git a/src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_current_day_swap.sql b/src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_current_day_swap.sql new file mode 100644 index 000000000..0d7d33e25 --- /dev/null +++ b/src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_current_day_swap.sql @@ -0,0 +1,52 @@ +-- Fields marked with ## are non relevant to this test, and set to meaningless values. +insert into swap_events values ( + 2, -- ## + '1', -- ## + '1', -- ## + now(), -- Transaction timestamp + now(), -- ## + + -- Market and state metadata. + 777701, -- Market ID + '\\xDEADBEEF'::bytea, -- ## + '{""}', -- ## + now(), -- ## + 2, -- ## + 'swap_buy', -- ## + '0xaaa101', -- ## + + -- Swap event data. + '', -- ## + '', -- ## + 0, -- ## + 0, -- ## + false, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 13835058055282163712000, -- Average execution price Q64 + 0, -- ## + false, -- ## + false, -- ## + + -- State event data. + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0 -- ## +) diff --git a/src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_market_state.sql b/src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_market_state.sql new file mode 100644 index 000000000..5cd41f204 --- /dev/null +++ b/src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_market_state.sql @@ -0,0 +1,44 @@ +insert into market_latest_state_event values ( + 2, -- ## + '1', -- ## + '1', -- ## + now(), -- Transaction timestamp + now(), -- ## + + -- Market and state metadata. + 777701, -- Market ID + '\\xDEADBEEF'::bytea, -- ## + '{""}', -- ## + now(), -- Bump time + 2, -- ## + 'swap_buy', -- ## + '0xaaa101', -- ## + + -- State event data. + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + false, -- ## + 13835058055282163712000, -- Last swap average execution price + 0, -- ## + 0, -- ## + 0, -- ## + now(), -- ## + + 0, -- ## + false, -- ## + 92233720368547758080000000000000 -- Volume in 1m state tracker +) diff --git a/src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_past_day_swap.sql b/src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_past_day_swap.sql new file mode 100644 index 000000000..9e432e3f5 --- /dev/null +++ b/src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_past_day_swap.sql @@ -0,0 +1,52 @@ +-- Fields marked with ## are non relevant to this test, and set to meaningless values. +insert into swap_events values ( + 1, -- ## + '1', -- ## + '1', -- ## + now() - interval '1 day 1 hour', -- Transaction timestamp + now(), -- ## + + -- Market and state metadata. + 777701, -- Market ID + '\\xDEADBEEF'::bytea, -- ## + '{""}', -- ## + now(), -- ## + 1, -- ## + 'swap_buy', -- ## + '0xaaa101', -- ## + + -- Swap event data. + '', -- ## + '', -- ## + 0, -- ## + 0, -- ## + false, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 9223372036854775808000, -- Average execution price Q64 + 0, -- ## + false, -- ## + false, -- ## + + -- State event data. + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0 -- ## +) diff --git a/src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_earlier_swap.sql b/src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_earlier_swap.sql new file mode 100644 index 000000000..c330c73ea --- /dev/null +++ b/src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_earlier_swap.sql @@ -0,0 +1,52 @@ +-- Fields marked with ## are non relevant to this test, and set to meaningless values. +insert into swap_events values ( + 1, -- ## + '1', -- ## + '1', -- ## + now() - interval '1 day 1 hour', -- Transaction timestamp + now(), -- ## + + -- Market and state metadata. + 777702, -- Market ID + '\\xDEADBEEF'::bytea, -- ## + '{""}', -- ## + now(), -- ## + 1, -- ## + 'swap_buy', -- ## + '0xaaa101', -- ## + + -- Swap event data. + '', -- ## + '', -- ## + 0, -- ## + 0, -- ## + false, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 18446744073709551616000, -- Average execution price Q64 + 0, -- ## + false, -- ## + false, -- ## + + -- State event data. + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0 -- ## +) diff --git a/src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_later_swap.sql b/src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_later_swap.sql new file mode 100644 index 000000000..6ace1528b --- /dev/null +++ b/src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_later_swap.sql @@ -0,0 +1,52 @@ +-- Fields marked with ## are non relevant to this test, and set to meaningless values. +insert into swap_events values ( + 2, -- ## + '1', -- ## + '1', -- ## + now(), -- Transaction timestamp + now(), -- ## + + -- Market and state metadata. + 777702, -- Market ID + '\\xDEADBEEF'::bytea, -- ## + '{""}', -- ## + now(), -- ## + 2, -- ## + 'swap_buy', -- ## + '0xaaa101', -- ## + + -- Swap event data. + '', -- ## + '', -- ## + 0, -- ## + 0, -- ## + false, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 4611686018427387904000, -- Average execution price Q64 + 0, -- ## + false, -- ## + false, -- ## + + -- State event data. + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0 -- ## +) diff --git a/src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_market_state.sql b/src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_market_state.sql new file mode 100644 index 000000000..b5ba6213f --- /dev/null +++ b/src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_market_state.sql @@ -0,0 +1,44 @@ +insert into market_latest_state_event values ( + 2, -- ## + '1', -- ## + '1', -- ## + now(), -- Transaction timestamp + now(), -- ## + + -- Market and state metadata. + 777702, -- Market ID + '\\xDEADBEEF'::bytea, -- ## + '{""}', -- ## + now(), -- Bump time + 2, -- ## + 'swap_buy', -- ## + '0xaaa101', -- ## + + -- State event data. + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + 0, -- ## + false, -- ## + 4611686018427387904000, -- Last swap average execution price + 0, -- ## + 0, -- ## + 0, -- ## + now(), -- ## + + 0, -- ## + false, -- ## + 73786976294838206464000000000000 -- Volume in 1m state tracker +) From 814ea80fc5dca816399805422ef07e79cbb54dfd Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Fri, 1 Nov 2024 16:12:22 +0100 Subject: [PATCH 07/94] [ECO-2351] Clean up package.json commands (#316) --- .github/actions/ts-run-tests/action.yaml | 37 --------------------- .github/workflows/frontend-tests.yaml | 31 ++++-------------- .github/workflows/sdk-tests.yaml | 29 +++++++++++------ cfg/cspell-dictionary.txt | 2 ++ package.json | 28 ++++++++++++---- src/typescript/frontend/package.json | 9 ++---- src/typescript/package.json | 41 +++++++++++------------- src/typescript/sdk/package.json | 5 +-- 8 files changed, 73 insertions(+), 109 deletions(-) delete mode 100644 .github/actions/ts-run-tests/action.yaml diff --git a/.github/actions/ts-run-tests/action.yaml b/.github/actions/ts-run-tests/action.yaml deleted file mode 100644 index 9e4bc682b..000000000 --- a/.github/actions/ts-run-tests/action.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# cspell:word TMPDIR ---- -description: | - Run the emojicoin dot fun TypeScript E2E and unit tests on a local network -name: 'Run Typescript E2E tests' -runs: - steps: - # Install node and pnpm. - - uses: 'actions/setup-node@v4' - with: - node-version-file: '${{ env.TS_DIR }}/.node-version' - registry-url: 'https://registry.npmjs.org' - - uses: 'pnpm/action-setup@v4' - with: - package_json_file: '${{ env.TS_DIR }}/package.json' - # Run package install. If install fails, it probably means the updated - # lockfile was not included in the commit. - - run: | - cd ${{ env.TS_DIR }} && pnpm install --frozen-lockfile - shell: 'bash' - - env: - # This is important for ensuring that any temporary directories are - # created in a location that actually supports mounting. - # See here: https://stackoverflow.com/a/76523941/3846032. - TMPDIR: '${{ runner.temp }}' - name: 'pnpm-test' - uses: 'nick-fields/retry@7f8f3d9f0f62fe5925341be21c2e8314fd4f7c7c' - with: - command: 'cd ${{ env.TS_DIR }} && pnpm run test' - max_attempts: 1 - timeout_minutes: 10 - - if: 'failure()' - name: 'Print local testnet logs on failure' - run: 'cat ${{ runner.temp }}/local-testnet-logs.txt' - shell: 'bash' - using: 'composite' -... diff --git a/.github/workflows/frontend-tests.yaml b/.github/workflows/frontend-tests.yaml index f5124ca58..694523d49 100644 --- a/.github/workflows/frontend-tests.yaml +++ b/.github/workflows/frontend-tests.yaml @@ -7,46 +7,29 @@ jobs: runs-on: 'ubuntu-latest' steps: - uses: 'actions/checkout@v4' + with: + submodules: 'false' - uses: 'actions/setup-node@v4' with: - node-version: 'lts/*' + node-version-file: 'src/typescript/.node-version' - name: 'Install dependencies' run: 'npm install -g pnpm && pnpm install' - - env: - GITHUB_ACCESS_TOKEN: '${{ secrets.TRADING_VIEW_REPO_ACCESS_TOKEN }}' - TRADING_VIEW_REPO_OWNER: '${{ secrets.TRADING_VIEW_REPO_OWNER }}' - name: 'Test' - run: 'echo -n $TRADING_VIEW_REPO_OWNER | wc -c' - env: GITHUB_ACCESS_TOKEN: '${{ secrets.TRADING_VIEW_REPO_ACCESS_TOKEN }}' TRADING_VIEW_REPO_OWNER: '${{ secrets.TRADING_VIEW_REPO_OWNER }}' name: 'Clone submodule' run: 'pnpm run submodule' - - name: 'Run pnpm i' - run: 'pnpm i' - name: 'Install Playwright Browsers' run: 'pnpm run playwright:install' - - name: 'Copy env file' - run: 'cp ci.env .env' - - name: 'Copy docker env file' - run: 'cp ../docker/example.local.env ../docker/.env' - name: 'Run Playwright tests' - run: 'pnpm run e2e:frontend' - - if: 'always()' - uses: 'actions/upload-artifact@v4' - with: - name: 'playwright-report' - path: 'playwright-report/' - retention-days: 30 - timeout-minutes: 60 + run: 'pnpm run test:frontend' + timeout-minutes: 15 name: 'Run the frontend tests' "on": - pull_request: - branches: - - 'main' - - 'production' + pull_request: null push: branches: - 'main' - 'production' + workflow_dispatch: null ... diff --git a/.github/workflows/sdk-tests.yaml b/.github/workflows/sdk-tests.yaml index ee37dddad..b4107a4ba 100644 --- a/.github/workflows/sdk-tests.yaml +++ b/.github/workflows/sdk-tests.yaml @@ -1,25 +1,36 @@ --- -env: - NEXT_PUBLIC_MODULE_ADDRESS: >- - 0xf000d910b99722d201c6cf88eb7d1112b43475b9765b118f289b5d65d919000d - PUBLISHER_PRIVATE_KEY: >- - 0xeaa964d1353b075ac63b0c5a0c1e92aa93355be1402f6077581e37e2a846105e - TS_DIR: 'src/typescript' jobs: sdk-tests: - permissions: - contents: 'write' + defaults: + run: + working-directory: 'src/typescript' runs-on: 'ubuntu-latest' steps: - uses: 'actions/checkout@v4' with: submodules: 'false' + - uses: 'actions/setup-node@v4' + with: + node-version-file: 'src/typescript/.node-version' + - name: 'Install dependencies' + run: 'npm install -g pnpm && pnpm install' - name: 'Install the latest Aptos CLI' # yamllint disable-line rule:line-length uses: 'aptos-labs/aptos-core/.github/actions/get-latest-cli@8792eefecd537c33fb879984635a0762838e2329' with: destination_directory: '/usr/local/bin' - - uses: './.github/actions/ts-run-tests' + - env: + NEXT_PUBLIC_MODULE_ADDRESS: >- + 0xf000d910b99722d201c6cf88eb7d1112b43475b9765b118f289b5d65d919000d + PUBLISHER_PRIVATE_KEY: >- + 0xeaa964d1353b075ac63b0c5a0c1e92aa93355be1402f6077581e37e2a846105e + name: 'Run Playwright tests' + run: 'pnpm run test:sdk' + - if: 'failure()' + name: 'Print local testnet logs on failure' + run: 'cat ${{ runner.temp }}/local-testnet-logs.txt' + shell: 'bash' + timeout-minutes: 15 name: 'Run the SDK tests' 'on': pull_request: null diff --git a/cfg/cspell-dictionary.txt b/cfg/cspell-dictionary.txt index 2d5955e42..1f7bbae7f 100644 --- a/cfg/cspell-dictionary.txt +++ b/cfg/cspell-dictionary.txt @@ -1,3 +1,4 @@ +ADBEEF aland allowlister aptn @@ -11,6 +12,7 @@ bouvet buildtime buildx burkina +bytea caicos clamm clipperton diff --git a/package.json b/package.json index a9d0f23bf..fd6d96bb7 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,33 @@ { "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a", "scripts": { + "build": "pnpm --prefix src/typescript run build", + "build:debug": "pnpm --prefix src/typescript run build:debug", + "check": "pnpm --prefix src/typescript run check", + "clean": "pnpm --prefix src/typescript run clean", + "clean:full": "pnpm --prefix src/typescript run clean:full", "dev": "pnpm --prefix src/typescript run dev", - "down": "pnpm --prefix src/typescript run down", - "e2e:testnet": "pnpm --prefix src/typescript run e2e:testnet", + "dev:debug": "pnpm --prefix src/typescript run dev:debug", + "dev:debug-verbose": "pnpm --prefix src/typescript run dev:debug-verbose", + "docker:prune": "pnpm --prefix src/typescript run docker:prune", + "docker:restart": "pnpm --prefix src/typescript run docker:restart", + "docker:up": "pnpm --prefix src/typescript run docker:up", "format": "pnpm --prefix src/typescript run format", + "format:check": "pnpm --prefix src/typescript run format:check", "lint": "pnpm --prefix src/typescript run lint", "lint:fix": "pnpm --prefix src/typescript run lint:fix", - "prune": "pnpm --prefix src/typescript run prune", - "restart": "pnpm --prefix src/typescript run restart", + "load-env": "pnpm --prefix src/typescript run load-env", + "load-env:test": "pnpm --prefix src/typescript run load-env:test", + "playwright:install": "pnpm --prefix src/typescript run playwright:install", "start": "pnpm --prefix src/typescript run start", + "submodule": "pnpm --prefix src/typescript run submodule", "test": "pnpm --prefix src/typescript run test", "test:debug": "pnpm --prefix src/typescript run test:debug", - "test:verbose": "pnpm --prefix src/typescript run test:verbose", - "unit-test": "pnpm --prefix src/typescript run unit-test", - "up": "pnpm --prefix src/typescript run up" + "test:frontend": "pnpm --prefix src/typescript run test:frontend", + "test:frontend:e2e": "pnpm --prefix src/typescript run test:frontend:e2e", + "test:sdk": "pnpm --prefix src/typescript run test:sdk", + "test:sdk:e2e": "pnpm --prefix src/typescript run test:sdk:e2e", + "test:sdk:unit": "pnpm --prefix src/typescript run test:sdk:unit", + "test:verbose": "pnpm --prefix src/typescript run test:verbose" } } diff --git a/src/typescript/frontend/package.json b/src/typescript/frontend/package.json index ab91ee419..f0156999d 100644 --- a/src/typescript/frontend/package.json +++ b/src/typescript/frontend/package.json @@ -93,20 +93,15 @@ "build:debug": "BUILD_DEBUG=true next build --no-lint --no-mangling --debug", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist && rm -rf .next", "dev": "NODE_OPTIONS='--inspect' next dev --turbo --port 3001", - "e2e-chromium": "playwright test --project=chromium", - "e2e-firefox": "playwright test --project=firefox", - "e2e-webkit": "playwright test --project=webkit", - "e2e:frontend": "playwright test --project=firefox", "format": "pnpm _format --write", "format:check": "pnpm _format --check", "lint": "eslint --max-warnings=0 -c .eslintrc.js --ext .js,.jsx,.ts,.tsx .", "lint:fix": "pnpm run lint --fix", "playwright:install": "playwright install --with-deps", - "pre-commit": "pnpm run pre-commit:install && pnpm run pre-commit:run", - "pre-commit:install": "pre-commit install -c ../../../cfg/pre-commit-config.yaml", - "pre-commit:run": "pre-commit run --all-files -c ../../../cfg/pre-commit-config.yaml", "start": "next start --port 3001", "submodule": "./submodule.sh", + "test": "pnpm run test:e2e", + "test:e2e": "playwright test --project=firefox", "vercel-install": "./submodule.sh && pnpm i" }, "version": "0.0.1-alpha" diff --git a/src/typescript/package.json b/src/typescript/package.json index 105641293..f8b5e2e3c 100644 --- a/src/typescript/package.json +++ b/src/typescript/package.json @@ -17,39 +17,34 @@ "keyv": "npm:@keyvhq/core@2.1.1" }, "scripts": { - "build": "pnpm i && pnpm load-env -- turbo run build", - "build:debug": "pnpm i && pnpm load-env -- turbo run build:debug", + "build": "pnpm i && pnpm load-env:test -- turbo run build", + "build:debug": "pnpm i && pnpm load-env:test -- turbo run build:debug", "check": "turbo run check", - "clean": "turbo run clean --no-cache --force && rm -rf .turbo && rm -rf sdk/.turbo && rm -rf frontend/.turbo && rm -rf frontend/.next", + "clean": "turbo run clean --no-cache --force && rm -rf .turbo", + "clean:full": "pnpm run clean && rm -rf node_modules && rm -rf sdk/node_modules && rm -rf frontend/node_modules", "dev": "pnpm load-env -- turbo run dev --force --parallel --continue", - "dev:debug": "pnpm dotenv -v FETCH_DEBUG=true -- pnpm run dev", - "dev:debug-verbose": "pnpm dotenv -v FETCH_DEBUG_VERBOSE=true -- pnpm run dev", - "down": "pnpm run prune", - "e2e:frontend": "pnpm run load-env:e2e-frontend -- turbo run e2e:frontend --filter @econia-labs/emojicoin-frontend --log-prefix none", + "dev:debug": "FETCH_DEBUG=true pnpm load-env -- pnpm run dev", + "dev:debug-verbose": "FETCH_DEBUG_VERBOSE=true pnpm load-env -- pnpm run dev", + "docker:prune": "../docker/utils/prune.sh --reset-localnet --yes", + "docker:restart": "pnpm run docker:prune && pnpm run docker:up", + "docker:up": "docker compose -f ../docker/compose.local.yaml --env-file ../docker/compose.local.yaml up -d", "format": "turbo run format -- --write", "format:check": "turbo run format -- --check", - "full-clean": "pnpm run clean && rm -rf node_modules && rm -rf sdk/node_modules && rm -rf frontend/node_modules", "lint": "turbo run lint", "lint:fix": "turbo run lint -- --fix", - "load-env": "dotenv -e .env.local -e .env -e .env.example -e ../docker/example.local.env -e ../docker/.env", - "load-env:e2e-frontend": "dotenv -e .env.local -e .env -e .env.example -e ../docker/example.local.env -e ../docker/.env -e ci.env -v NODE_ENV=test", - "load-env:test": "dotenv -e .env.local -e .env -e .env.example -e ../docker/example.local.env -e ../docker/.env -e ci.env", - "load-env:test-debug": "dotenv -e .env.local -e .env -e .env.example -e ../docker/example.local.env -e ../docker/.env -e ci.env -v FETCH_DEBUG=true", - "load-env:test-verbose": "dotenv -e .env.local -e .env -e .env.example -e ../docker/example.local.env -e ../docker/.env -e ci.env -v FETCH_DEBUG=true VERBOSE_TEST_LOGS=true", - "load-env:unit-test": "dotenv -e .env.local -e .env -e .env.example -e ../docker/example.local.env -e ../docker/.env -e ci.env -v NO_TEST_SETUP=true", + "load-env": "dotenv -e .env", + "load-env:test": "dotenv -e ci.env", "playwright:install": "turbo run playwright:install", - "prune": "../docker/utils/prune.sh --reset-localnet --yes", - "restart": "pnpm run down && pnpm run up", - "start": "dotenv -e .env.local -e .env -- turbo run start", + "start": "pnpm load-env -- turbo run start", "submodule": "turbo run submodule", "test": "pnpm run load-env:test -- turbo run test --force", - "test:debug": "pnpm run load-env:test-debug -- turbo run test --force", + "test:debug": "FETCH_DEBUG=true pnpm run load-env:test -- turbo run test --force", "test:frontend": "pnpm run load-env:test -- turbo run test --filter @econia-labs/emojicoin-frontend --log-prefix none", - "test:parallel": "pnpm run load-env:test -- turbo run test:parallel --force --log-prefix none", - "test:sequential": "pnpm run load-env:test -- turbo run test:sequential --force --log-prefix none", - "test:verbose": "pnpm run load-env:test-verbose -- turbo run test --force", - "unit-test": "pnpm run load-env:unit-test -- turbo run unit-test --force --log-prefix none --log-order grouped", - "up": "docker compose -f ../docker/compose.local.yaml up -d" + "test:frontend:e2e": "pnpm run load-env:test -- turbo run test:e2e --filter @econia-labs/emojicoin-frontend --log-prefix none", + "test:sdk": "pnpm run load-env:test -- turbo run test --filter @econia-labs/emojicoin-sdk --log-prefix none", + "test:sdk:e2e": "pnpm run load-env:unit-test -- turbo run test:e2e --filter @econia-labs/emojicoin-sdk --force --log-prefix none --log-order grouped", + "test:sdk:unit": "NO_TEST_SETUP=true pnpm run load-env:test -- turbo run test:unit --force --log-prefix none --log-order grouped", + "test:verbose": "FETCH_DEBUG=true VERBOSE_TEST_LOGS=true pnpm run load-env:test -- turbo run test --force" }, "version": "0.0.0", "workspaces": [ diff --git a/src/typescript/sdk/package.json b/src/typescript/sdk/package.json index 1fc23d47a..1fc590b2e 100644 --- a/src/typescript/sdk/package.json +++ b/src/typescript/sdk/package.json @@ -66,10 +66,11 @@ "pre-commit": "pnpm run pre-commit:install && pnpm run pre-commit:run", "pre-commit:install": "pre-commit install -c ../../../cfg/pre-commit-config.yaml", "pre-commit:run": "pre-commit run --all-files -c ../../../cfg/pre-commit-config.yaml", - "test": "pnpm run test:parallel && pnpm run test:sequential", + "test": "pnpm run test:parallel && pnpm run test:sequential && pnpm run test:unit", + "test:e2e": "pnpm run test:parallel && pnpm run test:sequential", "test:parallel": "pnpm jest --testPathIgnorePatterns=tests/e2e/broker", "test:sequential": "pnpm jest --runInBand tests/e2e/broker", - "unit-test": "pnpm jest tests/unit" + "test:unit": "pnpm jest tests/unit" }, "typings": "dist/src/index.d.ts", "version": "0.0.1" From 871849ad248afc24b511882b1c404f58ad8b45f8 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:08:04 -0800 Subject: [PATCH 08/94] [ECO-2320] Add new production stack (#319) Co-authored-by: alnoki <43892045+alnoki@users.noreply.github.com> --- src/cloud-formation/deploy-indexer-main.yaml | 2 +- .../deploy-indexer-production.yaml | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/cloud-formation/deploy-indexer-production.yaml diff --git a/src/cloud-formation/deploy-indexer-main.yaml b/src/cloud-formation/deploy-indexer-main.yaml index 6ee698df8..03a2d1ffd 100644 --- a/src/cloud-formation/deploy-indexer-main.yaml +++ b/src/cloud-formation/deploy-indexer-main.yaml @@ -20,7 +20,7 @@ parameters: EnableWafRulesWebSocket: 'false' Environment: 'main' Network: 'testnet' - ProcessorImageVersion: '0.8.1' + ProcessorImageVersion: '0.9.1' VpcStackName: 'emoji-vpc' tags: null template-file-path: 'src/cloud-formation/indexer.cfn.yaml' diff --git a/src/cloud-formation/deploy-indexer-production.yaml b/src/cloud-formation/deploy-indexer-production.yaml new file mode 100644 index 000000000..3371807b6 --- /dev/null +++ b/src/cloud-formation/deploy-indexer-production.yaml @@ -0,0 +1,27 @@ +--- +parameters: + BrokerImageVersion: '0.9.1' + DeployAlb: 'true' + DeployAlbDnsRecord: 'true' + DeployBastionHost: 'true' + DeployBroker: 'true' + DeployContainers: 'true' + DeployDb: 'true' + DeployNlb: 'true' + DeployNlbVpcLink: 'true' + DeployPostgrest: 'true' + DeployProcessor: 'true' + DeployRestApi: 'true' + DeployRestApiDnsRecord: 'true' + DeployStack: 'true' + DeployWaf: 'false' + EnableWafRulesGeneral: 'false' + EnableWafRulesRestApi: 'false' + EnableWafRulesWebSocket: 'false' + Environment: 'production' + Network: 'testnet' + ProcessorImageVersion: '0.9.1' + VpcStackName: 'emoji-vpc' +tags: null +template-file-path: 'src/cloud-formation/indexer.cfn.yaml' +... From e1e7a60bddb4668b99228164ceac8a473e5f2bba Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:22:06 -0800 Subject: [PATCH 09/94] [ECO-2357] Fix the chart on Petra mobile (#321) --- src/typescript/README.md | 5 +---- src/typescript/frontend/src/components/charts/const.ts | 1 + src/typescript/frontend/src/lib/utils/allowlist.ts | 6 +++++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/typescript/README.md b/src/typescript/README.md index ee9c06c6e..93eaba5b8 100644 --- a/src/typescript/README.md +++ b/src/typescript/README.md @@ -25,15 +25,12 @@ To avoid having to define environment variables in multiple places, we've intentionally omitted environment variables from the `frontend` directory to enforce that the project be built and run in a certain order. -## Copy the `example.env` file to an `.env` or `.env.local` file +## Copy the `example.env` file to a `.env` file Most commands load `.env.local` first then `.env`, so copy the environment example file to your own file. ```shell -# With the highest precedence. -cp example.env .env.local -# or just `.env.`, which will be superseded by `.env.local` in loading order cp example.env .env ``` diff --git a/src/typescript/frontend/src/components/charts/const.ts b/src/typescript/frontend/src/components/charts/const.ts index c631802ea..a7acf19e2 100644 --- a/src/typescript/frontend/src/components/charts/const.ts +++ b/src/typescript/frontend/src/components/charts/const.ts @@ -47,6 +47,7 @@ export const WIDGET_OPTIONS: Omit { ? (addressIn as `0x${string}`) : (`0x${addressIn}` as const); - return isInGalxeCampaign(address) || isOnCustomAllowlist(address); + const [inCampaign, onAllowlist] = await Promise.all([ + isInGalxeCampaign(address), + isOnCustomAllowlist(address), + ]); + return inCampaign || onAllowlist; } export const isInGalxeCampaign = async (address: `0x${string}`): Promise => { From 48ee2e80625baa8a56363e41227ab60016b50efd Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:37:22 -0800 Subject: [PATCH 10/94] [ECO-2362] Fix flaky playwright tests (#322) --- .github/workflows/frontend-tests.yaml | 13 +++++++ .gitignore | 1 + src/typescript/frontend/playwright.config.ts | 11 +++--- .../frontend/tests/e2e/market-order.spec.ts | 24 ++++++++----- .../frontend/tests/e2e/search.spec.ts | 19 ++++++----- src/typescript/frontend/tsconfig.json | 3 +- src/typescript/package.json | 4 +-- .../sdk/src/client/emojicoin-client.ts | 34 +++++++++++++++++-- .../utils/test/docker/docker-test-harness.ts | 10 ++++++ src/typescript/turbo.json | 10 ++---- 10 files changed, 95 insertions(+), 34 deletions(-) diff --git a/.github/workflows/frontend-tests.yaml b/.github/workflows/frontend-tests.yaml index 694523d49..9c6fc23bc 100644 --- a/.github/workflows/frontend-tests.yaml +++ b/.github/workflows/frontend-tests.yaml @@ -23,6 +23,19 @@ jobs: run: 'pnpm run playwright:install' - name: 'Run Playwright tests' run: 'pnpm run test:frontend' + - if: '${{ failure() }}' + name: 'Upload screenshots as artifacts' + uses: 'actions/upload-artifact@v4' + with: + name: 'playwright-screenshots' + path: 'src/typescript/screenshots/' + - if: '${{ !cancelled() }}' + name: 'Upload Playwright report as artifact' + uses: 'actions/upload-artifact@v4' + with: + name: 'playwright-report' + path: 'src/typescript/playwright-report/' + retention-days: 30 timeout-minutes: 15 name: 'Run the frontend tests' "on": diff --git a/.gitignore b/.gitignore index 5966c578d..505d79709 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ **/.env +**/.turbo diff --git a/src/typescript/frontend/playwright.config.ts b/src/typescript/frontend/playwright.config.ts index ccfcab617..061bba49b 100644 --- a/src/typescript/frontend/playwright.config.ts +++ b/src/typescript/frontend/playwright.config.ts @@ -5,7 +5,7 @@ import { defineConfig, devices } from "@playwright/test"; */ export default defineConfig({ testDir: "./tests/e2e", - /* Run tests in files in parallel */ + /* Run tests in files in parallel. */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, @@ -14,20 +14,24 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.GITHUB_ACTIONS ? "github" : "list", + reporter: [ + [process.env.GITHUB_ACTIONS ? "github" : "list"], + ["html", { outputFolder: "playwright-report" }], + ], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: "http://127.0.0.1:3001", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "on-first-retry", + trace: "retain-on-first-failure", launchOptions: { env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS || ""} --conditions=react-server`, }, + slowMo: 0, // Change this to 1000-3000 to slow the test down and see what's going on. }, }, @@ -47,7 +51,6 @@ export default defineConfig({ use: { ...devices["Desktop Chrome"] }, dependencies: ["setup"], }, - { name: "firefox", use: { ...devices["Desktop Firefox"] }, diff --git a/src/typescript/frontend/tests/e2e/market-order.spec.ts b/src/typescript/frontend/tests/e2e/market-order.spec.ts index 8c00837a0..c10528d88 100644 --- a/src/typescript/frontend/tests/e2e/market-order.spec.ts +++ b/src/typescript/frontend/tests/e2e/market-order.spec.ts @@ -10,13 +10,18 @@ test("check sorting order", async ({ page }) => { const markets = emojis.map((e) => [rat, SYMBOL_EMOJI_DATA.byName(e)!.emoji]); const client = new EmojicoinClient(); + client.view; // Register markets. // They all start with rat to simplify the search. + // Only register if it doesn't exist- this will occur on retries of the test. for (let i = 0; i < markets.length; i++) { - await client.register(user, markets[i]).then((res) => res.handle); + const exists = await client.view.marketExists(markets[i]); + if (!exists) { + await client.register(user, markets[i]); + } const amount = ((1n * ONE_APT_BIGINT) / 100n) * BigInt(10 ** (markets.length - i)); - await client.buy(user, markets[i], amount).then((res) => res.handle); + await client.buy(user, markets[i], amount); } await page.goto("/home"); @@ -52,6 +57,7 @@ test("check sorting order", async ({ page }) => { // Expect the sort by daily volume button to be visible. const dailyVolume = page.locator("#emoji-grid-header").getByText("24h Volume"); + await dailyVolume.waitFor({ state: "visible", timeout: 5000 }); expect(dailyVolume).toBeVisible(); // Sort by daily volume. @@ -61,7 +67,7 @@ test("check sorting order", async ({ page }) => { const patterns = names.map((e) => new RegExp(e)); // Expect the markets to be in order of daily volume. - marketGridItems = page.locator("#emoji-grid a").getByTitle(/RAT,/, { exact: true }); + marketGridItems = page.locator("#emoji-grid a").getByTitle(/RAT,/); expect(marketGridItems).toHaveText(patterns); // Click the sorting button. @@ -69,16 +75,18 @@ test("check sorting order", async ({ page }) => { // Expect the sort by bump order button to be visible. const bumpOrder = page.locator("#emoji-grid-header").getByText("Bump Order"); + await bumpOrder.waitFor({ state: "visible", timeout: 5000 }); expect(bumpOrder).toBeVisible(); + await page.screenshot({ path: "screenshots/test-failure.png" }); + // Sort by bump order. await bumpOrder.click(); - await page.screenshot(); - // Expect the markets to be in bump order. - marketGridItems = page.locator("#emoji-grid a").getByTitle(/RAT,/, { exact: true }); + marketGridItems = page.locator("#emoji-grid a").getByTitle(/RAT,/); + await marketGridItems.first().waitFor({ state: "visible", timeout: 5000 }); + await page.screenshot({ path: "screenshots/test-failure-2.png" }); + expect(marketGridItems.first()).toBeVisible(); expect(marketGridItems).toHaveText(patterns.reverse()); - - await page.screenshot(); }); diff --git a/src/typescript/frontend/tests/e2e/search.spec.ts b/src/typescript/frontend/tests/e2e/search.spec.ts index bac4615dd..1bf140705 100644 --- a/src/typescript/frontend/tests/e2e/search.spec.ts +++ b/src/typescript/frontend/tests/e2e/search.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from "@playwright/test"; import { EmojicoinClient } from "../../../sdk/src/client/emojicoin-client"; import { getFundedAccount } from "../../../sdk/src/utils/test/test-accounts"; -import { sleep, SYMBOL_EMOJI_DATA } from "../../../sdk/src"; +import { SYMBOL_EMOJI_DATA } from "../../../sdk/src"; test("check search results", async ({ page }) => { const user = getFundedAccount("666"); @@ -9,7 +9,11 @@ test("check search results", async ({ page }) => { const symbols = [cat, cat]; const client = new EmojicoinClient(); - await client.register(user, symbols).then((res) => res.handle); + // Register the market if it doesn't exist- it should only exist if the test is retried. + const exists = await client.view.marketExists(symbols); + if (!exists) { + await client.register(user, symbols); + } await page.goto("/home"); @@ -28,15 +32,12 @@ test("check search results", async ({ page }) => { await emojiSearch.fill("cat"); // Expect the "cat" emoji to be visible in the search results. - let emojiSearchCatButton = picker.getByLabel(cat).first(); + // Note: we must use `getByRole` with 'button' because this element is in the picker shadow DOM. + let emojiSearchCatButton = picker.getByRole("button", { name: cat, exact: true }); expect(emojiSearchCatButton).toBeVisible(); - // Search for the cat,cat market. - await emojiSearchCatButton.click({ force: true }); - - emojiSearchCatButton = picker.getByLabel(cat).first(); - expect(emojiSearchCatButton).toBeVisible(); - await emojiSearchCatButton.click({ force: true }); + // Search for the cat,cat market by clicking twice. + await emojiSearchCatButton.click({ force: true, clickCount: 2 }); // Click on the cat,cat market. const marketCard = page.getByText("cat,cat", { exact: true }); diff --git a/src/typescript/frontend/tsconfig.json b/src/typescript/frontend/tsconfig.json index 8b30e81e8..f1a2f479d 100644 --- a/src/typescript/frontend/tsconfig.json +++ b/src/typescript/frontend/tsconfig.json @@ -69,6 +69,7 @@ "./src/**/*.ts", "./src/**/*.tsx", ".next/types/**/*.ts", - "./dist/types/**/*.ts" + "./dist/types/**/*.ts", + "playwright.config.ts" ] } diff --git a/src/typescript/package.json b/src/typescript/package.json index f8b5e2e3c..8215fe55f 100644 --- a/src/typescript/package.json +++ b/src/typescript/package.json @@ -27,7 +27,7 @@ "dev:debug-verbose": "FETCH_DEBUG_VERBOSE=true pnpm load-env -- pnpm run dev", "docker:prune": "../docker/utils/prune.sh --reset-localnet --yes", "docker:restart": "pnpm run docker:prune && pnpm run docker:up", - "docker:up": "docker compose -f ../docker/compose.local.yaml --env-file ../docker/compose.local.yaml up -d", + "docker:up": "docker compose -f ../docker/compose.local.yaml --env-file ../docker/example.local.env up -d", "format": "turbo run format -- --write", "format:check": "turbo run format -- --check", "lint": "turbo run lint", @@ -39,7 +39,7 @@ "submodule": "turbo run submodule", "test": "pnpm run load-env:test -- turbo run test --force", "test:debug": "FETCH_DEBUG=true pnpm run load-env:test -- turbo run test --force", - "test:frontend": "pnpm run load-env:test -- turbo run test --filter @econia-labs/emojicoin-frontend --log-prefix none", + "test:frontend": " pnpm run load-env:test -- turbo run test --filter @econia-labs/emojicoin-frontend --log-prefix none", "test:frontend:e2e": "pnpm run load-env:test -- turbo run test:e2e --filter @econia-labs/emojicoin-frontend --log-prefix none", "test:sdk": "pnpm run load-env:test -- turbo run test --filter @econia-labs/emojicoin-sdk --log-prefix none", "test:sdk:e2e": "pnpm run load-env:unit-test -- turbo run test:e2e --filter @econia-labs/emojicoin-sdk --force --log-prefix none --log-order grouped", diff --git a/src/typescript/sdk/src/client/emojicoin-client.ts b/src/typescript/sdk/src/client/emojicoin-client.ts index da8b67a60..3f936abea 100644 --- a/src/typescript/sdk/src/client/emojicoin-client.ts +++ b/src/typescript/sdk/src/client/emojicoin-client.ts @@ -9,7 +9,7 @@ import { type WaitForTransactionOptions, } from "@aptos-labs/ts-sdk"; import { type ChatEmoji, type SymbolEmoji } from "../emoji_data/types"; -import { getEvents } from "../emojicoin_dot_fun"; +import { EmojicoinDotFun, getEvents } from "../emojicoin_dot_fun"; import { Chat, ProvideLiquidity, @@ -28,6 +28,7 @@ import { DEFAULT_REGISTER_MARKET_GAS_OPTIONS, INTEGRATOR_ADDRESS } from "../cons import { waitFor } from "../utils"; import { postgrest } from "../indexer-v2/queries"; import { TableName } from "../indexer-v2/types/json-types"; +import { type AnyNumberString } from "../types"; const { expect, Expect } = customExpect; @@ -100,18 +101,32 @@ export class EmojicoinClient { remove: this.removeLiquidity.bind(this), }; - public utils = { + public utils: { + emojisToHexStrings: typeof EmojicoinClient.prototype.emojisToHexStrings; + emojisToHexSymbol: typeof EmojicoinClient.prototype.emojisToHexSymbol; + getEmojicoinInfo: typeof EmojicoinClient.prototype.getEmojicoinInfo; + getTransactionEventData: typeof EmojicoinClient.prototype.getTransactionEventData; + } = { emojisToHexStrings: this.emojisToHexStrings.bind(this), emojisToHexSymbol: this.emojisToHexSymbol.bind(this), getEmojicoinInfo: this.getEmojicoinInfo.bind(this), getTransactionEventData: this.getTransactionEventData.bind(this), }; - public rewards = { + public rewards: { + buy: typeof EmojicoinClient.prototype.buyWithRewards; + sell: typeof EmojicoinClient.prototype.sellWithRewards; + } = { buy: this.buyWithRewards.bind(this), sell: this.sellWithRewards.bind(this), }; + public view: { + marketExists: typeof EmojicoinClient.prototype.isMarketRegisteredView; + } = { + marketExists: this.isMarketRegisteredView.bind(this), + }; + private integrator: AccountAddress; private integratorFeeRateBPs: number; @@ -228,6 +243,19 @@ export class EmojicoinClient { ); } + private async isMarketRegisteredView( + symbolEmojis: SymbolEmoji[], + ledgerVersion?: AnyNumberString + ) { + const { marketAddress } = this.getEmojicoinInfo(symbolEmojis); + const res = await EmojicoinDotFun.MarketMetadataByMarketAddress.view({ + aptos: this.aptos, + marketAddress, + ...(ledgerVersion ? { options: { ledgerVersion: BigInt(ledgerVersion) } } : {}), + }); + return typeof res.vec.pop() !== "undefined"; + } + private async swap( swapper: Account, symbolEmojis: SymbolEmoji[], diff --git a/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts b/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts index ac4189b31..44a3833f8 100644 --- a/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts +++ b/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts @@ -17,6 +17,7 @@ import { import { EMOJICOIN_INDEXER_URL } from "../../../server/env"; import { TableName } from "../../../indexer-v2/types/json-types"; import { readFileSync, writeFileSync } from "node:fs"; +import { execSync } from "node:child_process"; const LOCAL_COMPOSE_PATH = path.join(getGitRoot(), "src/docker", "compose.local.yaml"); const LOCAL_ENV_PATH = path.join(getGitRoot(), "src/docker", "example.local.env"); @@ -116,6 +117,15 @@ export class DockerTestHarness { await DockerTestHarness.remove(); const command = "docker"; + + // Always build the frontend container if we're using it. + if (frontend) { + execSync( + `docker compose -f ${LOCAL_COMPOSE_PATH} --env-file ${LOCAL_ENV_PATH} build frontend`, + { stdio: "inherit" } + ); + } + const args = [ "compose", "-f", diff --git a/src/typescript/turbo.json b/src/typescript/turbo.json index 7e3a230a3..a03f5fa9f 100644 --- a/src/typescript/turbo.json +++ b/src/typescript/turbo.json @@ -31,10 +31,6 @@ "cache": false, "persistent": true }, - "e2e:frontend": { - "cache": false, - "outputs": [] - }, "format": { "outputs": [] }, @@ -64,15 +60,15 @@ "cache": false, "outputs": [] }, - "test:parallel": { + "test:e2e": { "cache": false, "outputs": [] }, - "test:sequential": { + "test:parallel": { "cache": false, "outputs": [] }, - "unit-test": { + "test:sequential": { "cache": false, "outputs": [] } From f07a5beb9c6f40293a25fa3c5872e95e6475e84d Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Tue, 5 Nov 2024 19:10:23 +0100 Subject: [PATCH 11/94] [ECO-2371] Fix CI task name (#329) --- .github/workflows/sdk-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sdk-tests.yaml b/.github/workflows/sdk-tests.yaml index b4107a4ba..8025f7d69 100644 --- a/.github/workflows/sdk-tests.yaml +++ b/.github/workflows/sdk-tests.yaml @@ -24,7 +24,7 @@ jobs: 0xf000d910b99722d201c6cf88eb7d1112b43475b9765b118f289b5d65d919000d PUBLISHER_PRIVATE_KEY: >- 0xeaa964d1353b075ac63b0c5a0c1e92aa93355be1402f6077581e37e2a846105e - name: 'Run Playwright tests' + name: 'Run SDK tests' run: 'pnpm run test:sdk' - if: 'failure()' name: 'Print local testnet logs on failure' From a0ca53625617bbd0ec761458aa21d4d972558344 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Tue, 5 Nov 2024 19:18:33 +0100 Subject: [PATCH 12/94] [ECO-2369] Remove frontend console errors (#326) --- .../frontend/src/components/charts/PrivateChart.tsx | 2 +- .../src/components/pages/emoji-picker/EmojiPicker.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/typescript/frontend/src/components/charts/PrivateChart.tsx b/src/typescript/frontend/src/components/charts/PrivateChart.tsx index e6bb657f2..a78cb134c 100644 --- a/src/typescript/frontend/src/components/charts/PrivateChart.tsx +++ b/src/typescript/frontend/src/components/charts/PrivateChart.tsx @@ -67,7 +67,7 @@ const configurationData: DatafeedConfiguration = { * @param props * @returns */ -export const Chart = async (props: ChartContainerProps) => { +export const Chart = (props: ChartContainerProps) => { const tvWidget = useRef(); const ref = useRef(null); const router = useRouter(); diff --git a/src/typescript/frontend/src/components/pages/emoji-picker/EmojiPicker.tsx b/src/typescript/frontend/src/components/pages/emoji-picker/EmojiPicker.tsx index c69f96c00..17fa3133c 100644 --- a/src/typescript/frontend/src/components/pages/emoji-picker/EmojiPicker.tsx +++ b/src/typescript/frontend/src/components/pages/emoji-picker/EmojiPicker.tsx @@ -177,7 +177,7 @@ export default function EmojiPicker( } }, []); - const { drag, ...propsRest } = props; + const { drag, filterEmojis, ...propsRest } = props; return ( Loading...}> @@ -200,7 +200,7 @@ export default function EmojiPicker( theme="dark" perLine={8} exceptEmojis={[]} - filterEmojis={props.filterEmojis} + filterEmojis={filterEmojis} onEmojiSelect={(v: EmojiSelectorData) => { const newEmoji = unifiedCodepointsToEmoji(v.unified as `${string}-${string}`); insertEmojiTextInput([newEmoji]); From 84f920fd63b21cd1a58a3c337878cf1ede60e4f5 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:40:53 -0800 Subject: [PATCH 13/94] [ECO-2368] Fix test tsconfig.json linking for frontend, fix playwright screenshot/test report paths (#324) --- .github/workflows/frontend-tests.yaml | 4 ++-- src/typescript/frontend/.eslintrc.js | 2 +- src/typescript/frontend/.gitignore | 3 +++ src/typescript/frontend/tests/e2e/market-order.spec.ts | 4 ++-- src/typescript/frontend/tests/tsconfig.json | 7 +++++++ src/typescript/frontend/tsconfig.json | 3 +-- 6 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 src/typescript/frontend/tests/tsconfig.json diff --git a/.github/workflows/frontend-tests.yaml b/.github/workflows/frontend-tests.yaml index 9c6fc23bc..032acf150 100644 --- a/.github/workflows/frontend-tests.yaml +++ b/.github/workflows/frontend-tests.yaml @@ -28,13 +28,13 @@ jobs: uses: 'actions/upload-artifact@v4' with: name: 'playwright-screenshots' - path: 'src/typescript/screenshots/' + path: 'src/typescript/frontend/screenshots/' - if: '${{ !cancelled() }}' name: 'Upload Playwright report as artifact' uses: 'actions/upload-artifact@v4' with: name: 'playwright-report' - path: 'src/typescript/playwright-report/' + path: 'src/typescript/frontend/playwright-report/' retention-days: 30 timeout-minutes: 15 name: 'Run the frontend tests' diff --git a/src/typescript/frontend/.eslintrc.js b/src/typescript/frontend/.eslintrc.js index 46734c91f..6a41e681a 100644 --- a/src/typescript/frontend/.eslintrc.js +++ b/src/typescript/frontend/.eslintrc.js @@ -35,7 +35,7 @@ module.exports = { }, ecmaVersion: "latest", sourceType: "module", - project: ["tsconfig.json"], + project: ["tsconfig.json", "tests/tsconfig.json"], warnOnUnsupportedTypeScriptVersion: false, }, plugins: ["@typescript-eslint", "import", "prettier"], diff --git a/src/typescript/frontend/.gitignore b/src/typescript/frontend/.gitignore index 9de0974cb..c5ec732bf 100644 --- a/src/typescript/frontend/.gitignore +++ b/src/typescript/frontend/.gitignore @@ -52,3 +52,6 @@ yarn.lock /playwright-report/ /blob-report/ /playwright/.cache/ + +screenshots +playwright-report diff --git a/src/typescript/frontend/tests/e2e/market-order.spec.ts b/src/typescript/frontend/tests/e2e/market-order.spec.ts index c10528d88..2bdf913a3 100644 --- a/src/typescript/frontend/tests/e2e/market-order.spec.ts +++ b/src/typescript/frontend/tests/e2e/market-order.spec.ts @@ -78,7 +78,7 @@ test("check sorting order", async ({ page }) => { await bumpOrder.waitFor({ state: "visible", timeout: 5000 }); expect(bumpOrder).toBeVisible(); - await page.screenshot({ path: "screenshots/test-failure.png" }); + await page.screenshot({ path: "screenshots/market-order-1.png" }); // Sort by bump order. await bumpOrder.click(); @@ -86,7 +86,7 @@ test("check sorting order", async ({ page }) => { // Expect the markets to be in bump order. marketGridItems = page.locator("#emoji-grid a").getByTitle(/RAT,/); await marketGridItems.first().waitFor({ state: "visible", timeout: 5000 }); - await page.screenshot({ path: "screenshots/test-failure-2.png" }); + await page.screenshot({ path: "screenshots/market-order-2.png" }); expect(marketGridItems.first()).toBeVisible(); expect(marketGridItems).toHaveText(patterns.reverse()); }); diff --git a/src/typescript/frontend/tests/tsconfig.json b/src/typescript/frontend/tests/tsconfig.json new file mode 100644 index 000000000..c30c5f46b --- /dev/null +++ b/src/typescript/frontend/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "**/*.ts", + "../playwright.config.ts" + ] +} diff --git a/src/typescript/frontend/tsconfig.json b/src/typescript/frontend/tsconfig.json index f1a2f479d..8b30e81e8 100644 --- a/src/typescript/frontend/tsconfig.json +++ b/src/typescript/frontend/tsconfig.json @@ -69,7 +69,6 @@ "./src/**/*.ts", "./src/**/*.tsx", ".next/types/**/*.ts", - "./dist/types/**/*.ts", - "playwright.config.ts" + "./dist/types/**/*.ts" ] } From 02ac76d56d1a097e466f4abf5782bd30788b7e28 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 5 Nov 2024 19:57:47 -0800 Subject: [PATCH 14/94] [ECO-2372] Fix candlestick chart issues (#330) --- .../src/components/charts/PrivateChart.tsx | 31 ++++++++++++++----- .../frontend/src/lib/chart-utils/index.ts | 6 ++++ .../src/lib/store/event/candlestick-bars.ts | 10 ++++-- .../src/lib/store/event/event-store.ts | 10 ++++++ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/typescript/frontend/src/components/charts/PrivateChart.tsx b/src/typescript/frontend/src/components/charts/PrivateChart.tsx index a78cb134c..a1782d568 100644 --- a/src/typescript/frontend/src/components/charts/PrivateChart.tsx +++ b/src/typescript/frontend/src/components/charts/PrivateChart.tsx @@ -20,7 +20,7 @@ import { type Timezone, widget, } from "@static/charting_library"; -import { getClientTimezone } from "lib/chart-utils"; +import { getClientTimezone, hasTradingActivity } from "lib/chart-utils"; import { type ChartContainerProps } from "./types"; import { useRouter } from "next/navigation"; import { ROUTES } from "router/routes"; @@ -179,12 +179,13 @@ export const Chart = (props: ChartContainerProps) => { // Convert the market view data to `latestBar[]` and set the latest bars in our EventStore to those values. const latestBars = marketToLatestBars(marketResource); const marketEmojiData = toMarketEmojiData(marketResource.metadata.emojiBytes); + const symbolEmojis = marketEmojiData.emojis.map((e) => e.emoji); const marketMetadata: MarketMetadataModel = { marketID: marketResource.metadata.marketID, time: 0n, marketNonce: marketResource.sequenceInfo.nonce, trigger: Trigger.PackagePublication, // Make up some bunk trigger, since it should be clear it's made up. - symbolEmojis: marketEmojiData.emojis.map((e) => e.emoji), + symbolEmojis, marketAddress: marketResource.metadata.marketAddress, ...marketEmojiData, }; @@ -209,9 +210,11 @@ export const Chart = (props: ChartContainerProps) => { // some visual inconsistencies in the chart. const bars: Bar[] = data.reduce((acc: Bar[], event) => { const bar = toBar(event); - if (bar.time >= from * 1000 && bar.time <= to * 1000) { - if (acc.at(-1)) { - bar.open = acc.at(-1)!.close; + const inTimeRange = bar.time >= from * 1000 && bar.time <= to * 1000; + if (inTimeRange && hasTradingActivity(bar)) { + const prev = acc.at(-1); + if (prev) { + bar.open = prev.close; } acc.push(bar); } @@ -220,9 +223,23 @@ export const Chart = (props: ChartContainerProps) => { // Push the latest bar to the bars array if it exists and update its `open` value to be the previous bar's // `close` if it's not the first/only bar. + // This logic mirrors what we use in `createBarFrom[Swap|PeriodicState]` but we need it here because we + // update the latest bar based on the market view every time we fetch with `getBars`, not just when a new + // event comes in. if (latestBar) { - if (bars.at(-1)) { - latestBar.open = bars.at(-1)!.close; + const secondLatestBar = bars.at(-1); + if (secondLatestBar) { + // If the latest bar has no trading activity, set all of its fields to the previous bar's close. + if (!hasTradingActivity(latestBar)) { + latestBar.high = secondLatestBar.close; + latestBar.low = secondLatestBar.close; + latestBar.close = secondLatestBar.close; + } + if (secondLatestBar.close !== 0) { + latestBar.open = secondLatestBar.close; + } else { + latestBar.open = latestBar.close; + } } bars.push(latestBar); } diff --git a/src/typescript/frontend/src/lib/chart-utils/index.ts b/src/typescript/frontend/src/lib/chart-utils/index.ts index bbe56ca11..ea7883b7e 100644 --- a/src/typescript/frontend/src/lib/chart-utils/index.ts +++ b/src/typescript/frontend/src/lib/chart-utils/index.ts @@ -1,5 +1,8 @@ // cspell:word Kolkata // cspell:word Fakaofo + +import { type Bar } from "@static/charting_library/datafeed-api"; + /** * Retrieves the client's timezone based on the current system time offset. * @@ -70,3 +73,6 @@ export function getClientTimezone() { } return "Etc/UTC"; } + +export const hasTradingActivity = (bar: Bar) => + [bar.open, bar.high, bar.low, bar.close].some((price) => price !== 0); diff --git a/src/typescript/frontend/src/lib/store/event/candlestick-bars.ts b/src/typescript/frontend/src/lib/store/event/candlestick-bars.ts index 3d1b2d58a..4c8b1e165 100644 --- a/src/typescript/frontend/src/lib/store/event/candlestick-bars.ts +++ b/src/typescript/frontend/src/lib/store/event/candlestick-bars.ts @@ -59,7 +59,7 @@ export const toBar = (event: PeriodicStateEventModel): Bar => ({ high: q64ToBig(event.periodicState.highPriceQ64).toNumber(), low: q64ToBig(event.periodicState.lowPriceQ64).toNumber(), close: q64ToBig(event.periodicState.closePriceQ64).toNumber(), - volume: Number(event.periodicState.volumeQuote), + volume: Number(event.periodicState.volumeBase), }); export const toBars = (events: PeriodicStateEventModel | PeriodicStateEventModel[]) => @@ -75,7 +75,9 @@ export const createBarFromSwap = ( const periodStartTime = getPeriodStartTimeFromTime(market.time, period); return { time: Number(periodStartTime / 1000n), - open: previousClose ?? price, + // Only use previousClose if it's a truthy value, otherwise, new bars that follow bars with no + // trading activity will appear as a huge green candlestick because their open price is `0`. + open: previousClose ? previousClose : price, high: price, low: price, close: price, @@ -94,7 +96,9 @@ export const createBarFromPeriodicState = ( const price = q64ToBig(periodicState.closePriceQ64).toNumber(); return { time: periodEnumToRawDuration(period) / 1000, - open: previousClose ?? price, + // Only use previousClose if it's a truthy value, otherwise, new bars that follow bars with no + // trading activity will appear as a huge green candlestick because their open price is `0`. + open: previousClose ? previousClose : price, high: price, low: price, close: price, diff --git a/src/typescript/frontend/src/lib/store/event/event-store.ts b/src/typescript/frontend/src/lib/store/event/event-store.ts index a68f8e319..cf0e62a29 100644 --- a/src/typescript/frontend/src/lib/store/event/event-store.ts +++ b/src/typescript/frontend/src/lib/store/event/event-store.ts @@ -113,6 +113,16 @@ export const createEventStore = () => { const market = state.markets.get(symbol)!; latestBars.forEach((bar) => { const period = bar.period; + // A bar's open should never be zero, so use the previous bar if it exists and isn't 0, + // otherwise, use the existing current bar's close. + if (bar.open === 0) { + const prevLatestBarClose = market[period].latestBar?.close; + if (prevLatestBarClose) { + bar.open = prevLatestBarClose; + } else { + bar.open = bar.close; + } + } market[period].latestBar = bar; }); }); From 8b8bbec6b0c77d9a1cb7848ccb51ce3ea190eaeb Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Thu, 7 Nov 2024 19:48:58 +0100 Subject: [PATCH 15/94] [ECO-2382] Fix non-breaking chat messages (#332) --- .../components/chat/components/message-container/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/chat/components/message-container/index.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/chat/components/message-container/index.tsx index cc8b5055e..910d5637c 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/chat/components/message-container/index.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/chat/components/message-container/index.tsx @@ -53,7 +53,12 @@ const MessageContainer: React.FC = ({ - {message.text} + + {message.text} + From adbfce99823b10da2f3d2f8f0fefa2161d8bca5f Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Thu, 7 Nov 2024 21:38:04 +0100 Subject: [PATCH 16/94] [ECO-2256] Fix line break on market with three emojis (#328) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- src/typescript/frontend/src/app/global.css | 30 ------------------- .../home/components/table-card/TableCard.tsx | 8 +++-- .../components/table-header/index.tsx | 2 +- .../selects/dropdown-menu/index.tsx | 3 +- .../selects/dropdown-menu/module.css | 29 ++++++++++++++++++ .../frontend/src/components/table/index.tsx | 1 + .../frontend/src/components/text/index.tsx | 10 ------- .../frontend/src/components/text/theme.ts | 11 +++++++ .../frontend/src/components/text/types.ts | 1 + src/typescript/frontend/tailwind.config.js | 11 +++++++ 10 files changed, 61 insertions(+), 45 deletions(-) create mode 100644 src/typescript/frontend/src/components/selects/dropdown-menu/module.css diff --git a/src/typescript/frontend/src/app/global.css b/src/typescript/frontend/src/app/global.css index bebd917c6..8e33c9493 100644 --- a/src/typescript/frontend/src/app/global.css +++ b/src/typescript/frontend/src/app/global.css @@ -27,36 +27,6 @@ --error: #f3263e; } -.med-pixel-text { - font-size: 20px !important; - line-height: 25px; - font-family: var(--font-pixelar); -} - -@media screen and (min-width: 768px) { - .med-pixel-text { - font-size: 22px !important; - line-height: 27px; - font-family: var(--font-pixelar); - } -} - -@media screen and (min-width: 1024px) { - .med-pixel-text { - font-size: 24px !important; - line-height: 30px; - font-family: var(--font-pixelar); - } -} - -@media screen and (min-width: 1440px) { - .med-pixel-text { - font-size: 32px !important; - line-height: 40px; - font-family: var(--font-pixelar); - } -} - .med-pixel-search { width: 18px; } diff --git a/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx b/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx index 0de053d43..7a969dc13 100644 --- a/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx @@ -30,6 +30,10 @@ import LinkOrAnimationTrigger from "./LinkOrAnimationTrigger"; import { isMarketStateModel } from "@sdk/indexer-v2/types"; import { useEmojiPicker } from "context/emoji-picker-context"; import "./module.css"; +import { type SymbolEmojiData } from "@sdk/emoji_data"; + +const getFontSize = (emojis: SymbolEmojiData[]) => + emojis.length <= 2 ? ("pixel-heading-1" as const) : ("pixel-heading-1b" as const); const TableCard = ({ index, @@ -215,9 +219,9 @@ const TableCard = ({ - +
{symbol} - +
= ({ item, isLast, onClick }) => { )} = ({ onClick, diff --git a/src/typescript/frontend/src/components/selects/dropdown-menu/module.css b/src/typescript/frontend/src/components/selects/dropdown-menu/module.css new file mode 100644 index 000000000..5dda2bc73 --- /dev/null +++ b/src/typescript/frontend/src/components/selects/dropdown-menu/module.css @@ -0,0 +1,29 @@ +.med-pixel-text { + font-size: 20px !important; + line-height: 25px; + font-family: var(--font-pixelar) !important; +} + +@media screen and (min-width: 768px) { + .med-pixel-text { + font-size: 22px !important; + line-height: 27px; + font-family: var(--font-pixelar) !important; + } +} + +@media screen and (min-width: 1024px) { + .med-pixel-text { + font-size: 24px !important; + line-height: 30px; + font-family: var(--font-pixelar) !important; + } +} + +@media screen and (min-width: 1440px) { + .med-pixel-text { + font-size: 32px !important; + line-height: 40px; + font-family: var(--font-pixelar) !important; + } +} diff --git a/src/typescript/frontend/src/components/table/index.tsx b/src/typescript/frontend/src/components/table/index.tsx index 73ac69d70..804bb2458 100644 --- a/src/typescript/frontend/src/components/table/index.tsx +++ b/src/typescript/frontend/src/components/table/index.tsx @@ -60,6 +60,7 @@ export const Th = styled.td` z-index: 1; text-transform: uppercase; border-bottom: 1px solid ${({ theme }) => theme.colors.darkGray}; + align-content: center; &:nth-child(1) { ${ThInner} { diff --git a/src/typescript/frontend/src/components/text/index.tsx b/src/typescript/frontend/src/components/text/index.tsx index 701725059..9e939e28d 100644 --- a/src/typescript/frontend/src/components/text/index.tsx +++ b/src/typescript/frontend/src/components/text/index.tsx @@ -26,7 +26,6 @@ export const Text = styled.p.attrs(({ textScale = "display6" }) => ({ textScale, }))` color: ${({ theme, color }) => (color ? theme.colors[color] : theme.colors.white)}; - font-family: ${({ theme }) => theme.fonts.forma}; text-transform: ${({ textTransform }) => textTransform}; ${({ textScale }) => textScale && textStyles(textScale as keyof typeof scales)} @@ -39,15 +38,6 @@ export const Text = styled.p.attrs(({ textScale = "display6" }) => ({ ${layout} ${opacity} ${flexbox} - - ${({ className }) => - className && - css` - &.${className} { - font-family: inherit; - font-size: inherit; - } - `} `; export default Text; diff --git a/src/typescript/frontend/src/components/text/theme.ts b/src/typescript/frontend/src/components/text/theme.ts index 6b941d859..0db415b12 100644 --- a/src/typescript/frontend/src/components/text/theme.ts +++ b/src/typescript/frontend/src/components/text/theme.ts @@ -29,20 +29,28 @@ export const textStyles = (k: keyof typeof scales) => { [scales.display4]: ` font-size: 28px; line-height: 48px; + font-family: var(--font-forma); `, [scales.display5]: ` font-size: 20px; line-height: 48px; + font-family: var(--font-forma); `, [scales.display6]: ` font-size: 15px; line-height: 20px; + font-family: var(--font-forma); `, [scales.pixelHeading1]: ` font-size: 64px; line-height: 48px; font-family: var(--font-pixelar); `, + [scales.pixelHeading1b]: ` + font-size: 52px; + line-height: 48px; + font-family: var(--font-pixelar); + `, [scales.pixelHeading2]: ` font-size: 40px; line-height: 50px; @@ -71,14 +79,17 @@ export const textStyles = (k: keyof typeof scales) => { [scales.bodyLarge]: ` font-size: 16px; line-height: 18px; + font-family: var(--font-forma); `, [scales.bodySmall]: ` font-size: 12px; line-height: 18px; + font-family: var(--font-forma); `, [scales.bodyXSmall]: ` font-size: 10px; line-height: 18px; + font-family: var(--font-forma); `, }; diff --git a/src/typescript/frontend/src/components/text/types.ts b/src/typescript/frontend/src/components/text/types.ts index caffdce26..9c4f51148 100644 --- a/src/typescript/frontend/src/components/text/types.ts +++ b/src/typescript/frontend/src/components/text/types.ts @@ -23,6 +23,7 @@ export const scales = { display5: "display5", display6: "display6", pixelHeading1: "pixelHeading1", + pixelHeading1b: "pixelHeading1b", pixelHeading2: "pixelHeading2", pixelHeading3: "pixelHeading3", pixelHeading4: "pixelHeading4", diff --git a/src/typescript/frontend/tailwind.config.js b/src/typescript/frontend/tailwind.config.js index b67f138c1..b831a64a5 100644 --- a/src/typescript/frontend/tailwind.config.js +++ b/src/typescript/frontend/tailwind.config.js @@ -113,14 +113,17 @@ module.exports = { lineHeight: "65px", }, ".display-4": { + fontFamily: "var(--font-forma)", fontSize: "28px", lineHeight: "48px", }, ".display-5": { + fontFamily: "var(--font-forma)", fontSize: "20px", lineHeight: "48px", }, ".display-6": { + fontFamily: "var(--font-forma)", fontSize: "15px", lineHeight: "20px", }, @@ -129,6 +132,11 @@ module.exports = { fontSize: "64px", lineHeight: "48px", }, + ".pixel-heading-1b": { + fontFamily: "var(--font-pixelar)", + fontSize: "52px", + lineHeight: "48px", + }, ".pixel-heading-2": { fontFamily: "var(--font-pixelar)", fontSize: "40px", @@ -160,14 +168,17 @@ module.exports = { lineHeight: "18px", }, ".body-lg": { + fontFamily: "var(--font-forma)", fontSize: "16px", lineHeight: "18px", }, ".body-sm": { + fontFamily: "var(--font-forma)", fontSize: "12px", lineHeight: "18px", }, ".body-xs": { + fontFamily: "var(--font-forma)", fontSize: "10px", lineHeight: "18px", }, From fa98d095be278fa29ea341e5f7d282143e1b4d17 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Thu, 7 Nov 2024 23:31:33 +0100 Subject: [PATCH 17/94] [ECO-2370] Fix small bugs (#327) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- .../home/components/emoji-table/index.tsx | 7 +--- .../home/components/table-card/TableCard.tsx | 11 +++-- .../src/lib/queries/sorting/query-params.ts | 41 ------------------- 3 files changed, 9 insertions(+), 50 deletions(-) diff --git a/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx b/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx index c98f85049..97000d53d 100644 --- a/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx @@ -23,7 +23,7 @@ import { encodeEmojis } from "@sdk/emoji_data"; import { useEventStore, useUserSettings } from "context/event-store-context"; import { LiveClientGrid } from "./AnimatedClientGrid"; import useEvent from "@hooks/use-event"; -import { constructURLForHomePage, isHomePageURLDifferent } from "lib/queries/sorting/query-params"; +import { constructURLForHomePage } from "lib/queries/sorting/query-params"; import { AnimatePresence, motion } from "framer-motion"; import { EMOJI_GRID_ITEM_WIDTH } from "../const"; import { useGridRowLength } from "./hooks/use-grid-items-per-line"; @@ -66,18 +66,13 @@ const EmojiTable = (props: EmojiTableProps) => { }, [searchBytes]); const pushURL = useEvent((args?: { page?: number; sort?: SortMarketsBy; emojis?: string[] }) => { - const curr = new URLSearchParams(location.search); const newURL = constructURLForHomePage({ page: args?.page ?? page, sort: args?.sort ?? sort, searchBytes: encodeEmojis(args?.emojis ?? emojis), }); - // Always push the new URL to the history, but only refresh if the URL has actually changed in a meaningful way. router.push(newURL.toString(), { scroll: false }); - if (isHomePageURLDifferent(curr, newURL.searchParams)) { - router.refresh(); - } }); const handlePageChange = (page: number) => { diff --git a/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx b/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx index 7a969dc13..4b555d3f9 100644 --- a/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx @@ -28,7 +28,6 @@ import { } from "./animation-variants/grid-variants"; import LinkOrAnimationTrigger from "./LinkOrAnimationTrigger"; import { isMarketStateModel } from "@sdk/indexer-v2/types"; -import { useEmojiPicker } from "context/emoji-picker-context"; import "./module.css"; import { type SymbolEmojiData } from "@sdk/emoji_data"; @@ -59,7 +58,6 @@ const TableCard = ({ const animations = useEventStore( (s) => s.getMarket(emojis.map((e) => e.emoji))?.stateEvents ?? [] ); - const anySearchBytes = useEmojiPicker((s) => s.emojis.length > 0); // Keep track of whether or not the component is mounted to avoid animating an unmounted component. useLayoutEffect(() => { @@ -133,7 +131,14 @@ const TableCard = ({ // By default set this to 0, unless it's currently the left-most border. Sometimes we need to show a temporary border // though, which we handle in the layout animation begin/complete callbacks and in the outermost div's style prop. // Always show the left border when there's something in the search bar. - const borderLeftWidth = useMotionValue(curr.col === 0 ? 1 : anySearchBytes ? 1 : 0); + const borderLeftWidth = useMotionValue(curr.col === 0 ? 1 : 0); + + useEffect(() => { + if (curr.col === 0) { + borderLeftWidth.set(1); + } + /* eslint-disable react-hooks/exhaustive-deps */ + }, [curr]); return ( ; - -export const constructHomePageSearchParams = (searchParams: URLSearchParams) => { - const res = {} as HomePageSearchParams; - AllHomePageSearchParams.forEach((key) => { - const value = searchParams.get(key); - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - res[key] = value ?? (DefaultHomePageSearchParams[key] as any); - }); - return res; -}; - export const constructURLForHomePage = ({ page, sort, @@ -61,22 +39,3 @@ export const constructURLForHomePage = ({ return newURL; }; - -/** - * Check all the current and next url parameters using their default fallback values to see if the URL has - * actually changed. - */ -export const isHomePageURLDifferent = (curr: URLSearchParams, next: URLSearchParams) => { - if ((curr.get("page") ?? "1") !== (next.get("page") ?? "1")) { - return true; - } - if ( - (curr.get("sort") ?? SortMarketsBy.MarketCap) !== (next.get("sort") ?? SortMarketsBy.MarketCap) - ) { - return true; - } - if ((curr.get("q") ?? "0x") !== (next.get("q") ?? "0x")) { - return true; - } - return false; -}; From 5a828912012bf92a072faa490b73e3945f625757 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Fri, 8 Nov 2024 00:27:33 +0100 Subject: [PATCH 18/94] [ECO-2243] Fix NextJS config and cache (#283) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- src/typescript/frontend/next.config.mjs | 5 ++ .../frontend/src/app/candlesticks/route.ts | 80 +++++++++++++++++++ .../frontend/src/app/home/loading.tsx | 9 +++ src/typescript/frontend/src/app/home/page.tsx | 5 +- .../frontend/src/app/launch/loading.tsx | 9 +++ .../frontend/src/app/launch/page.tsx | 4 - .../src/app/market/[market]/loading.tsx | 9 +++ .../frontend/src/app/market/[market]/page.tsx | 9 +-- src/typescript/frontend/src/app/page.tsx | 47 +---------- .../src/app/pools/api/getPoolDataQuery.ts | 39 ++++----- .../frontend/src/app/pools/api/route.ts | 12 ++- .../frontend/src/app/pools/loading.tsx | 9 +++ .../frontend/src/app/pools/page.tsx | 28 ++++--- .../frontend/src/app/verify/loading.tsx | 9 +++ .../src/components/charts/PrivateChart.tsx | 35 +++++--- .../frontend/src/components/loading.tsx | 2 + src/typescript/frontend/src/middleware.ts | 4 + .../sdk/src/indexer-v2/types/json-types.ts | 2 +- 18 files changed, 209 insertions(+), 108 deletions(-) create mode 100644 src/typescript/frontend/src/app/candlesticks/route.ts create mode 100644 src/typescript/frontend/src/app/home/loading.tsx create mode 100644 src/typescript/frontend/src/app/launch/loading.tsx create mode 100644 src/typescript/frontend/src/app/market/[market]/loading.tsx create mode 100644 src/typescript/frontend/src/app/pools/loading.tsx create mode 100644 src/typescript/frontend/src/app/verify/loading.tsx diff --git a/src/typescript/frontend/next.config.mjs b/src/typescript/frontend/next.config.mjs index 2baaa2dab..38d24b26c 100644 --- a/src/typescript/frontend/next.config.mjs +++ b/src/typescript/frontend/next.config.mjs @@ -44,6 +44,11 @@ const nextConfig = { }, ...(DEBUG ? debugConfigOptions : {}), transpilePackages: ["@sdk"], + redirects: async () => ([{ + source: '/', + destination: '/home', + permanent: true, + }]), }; export default withBundleAnalyzer(nextConfig); diff --git a/src/typescript/frontend/src/app/candlesticks/route.ts b/src/typescript/frontend/src/app/candlesticks/route.ts new file mode 100644 index 000000000..187961515 --- /dev/null +++ b/src/typescript/frontend/src/app/candlesticks/route.ts @@ -0,0 +1,80 @@ +import { fetchPeriodicEventsSince } from "@/queries/market"; +import { type Period, toPeriod } from "@sdk/index"; +import { type PeriodicStateEventModel } from "@sdk/indexer-v2/types"; +import { type PeriodTypeFromDatabase } from "@sdk/indexer-v2/types/json-types"; +import { parseInt } from "lodash"; +import { unstable_cache } from "next/cache"; +import { type NextRequest } from "next/server"; +import { stringifyJSON } from "utils"; + +const CANDLESTICKS_LIMIT = 500; + +type QueryParams = { + marketID: number; + start: Date; + period: Period; + limit: number; +}; + +const getCandlesticks = async (params: QueryParams) => { + const { marketID, start, period, limit } = params; + const aggregate: PeriodicStateEventModel[] = []; + + while (aggregate.length < limit) { + const data = await fetchPeriodicEventsSince({ + marketID, + period, + start, + offset: aggregate.length, + limit: limit - aggregate.length, + }); + aggregate.push(...data); + if (data.length < limit) { + break; + } + } + + return stringifyJSON(aggregate); +}; + +const getCachedCandlesticks = unstable_cache(getCandlesticks, ["candlesticks"], { revalidate: 10 }); + +/* eslint-disable-next-line import/no-unused-modules */ +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const marketIDStr = searchParams.get("marketID"); + const startStr = searchParams.get("start"); + const periodStr = searchParams.get("period"); + const limitStr = searchParams.get("limit"); + + if (!marketIDStr || !startStr || !periodStr || !limitStr) { + return new Response("", { status: 400 }); + } + + if (isNaN(parseInt(marketIDStr))) { + return new Response("", { status: 400 }); + } + + if (isNaN(parseInt(startStr))) { + return new Response("", { status: 400 }); + } + + let period: Period; + try { + period = toPeriod(periodStr as PeriodTypeFromDatabase); + } catch { + return new Response("", { status: 400 }); + } + + if (isNaN(parseInt(limitStr)) || parseInt(limitStr) > CANDLESTICKS_LIMIT) { + return new Response("", { status: 400 }); + } + + const start = new Date(parseInt(startStr) * 1000); + const limit = parseInt(limitStr); + const marketID = parseInt(marketIDStr); + + const data = await getCachedCandlesticks({ marketID, start, period, limit }); + + return new Response(data); +} diff --git a/src/typescript/frontend/src/app/home/loading.tsx b/src/typescript/frontend/src/app/home/loading.tsx new file mode 100644 index 000000000..70189e4ce --- /dev/null +++ b/src/typescript/frontend/src/app/home/loading.tsx @@ -0,0 +1,9 @@ +"use client"; + +import React from "react"; + +import LoadingComponent from "components/loading"; + +export default function Loading() { + return ; +} diff --git a/src/typescript/frontend/src/app/home/page.tsx b/src/typescript/frontend/src/app/home/page.tsx index 0fd26f980..bdc4d5674 100644 --- a/src/typescript/frontend/src/app/home/page.tsx +++ b/src/typescript/frontend/src/app/home/page.tsx @@ -1,6 +1,5 @@ import { type HomePageParams, toHomePageParamsWithDefault } from "lib/routes/home-page-params"; import HomePageComponent from "./HomePage"; -import { REVALIDATION_TIME } from "lib/server-env"; import { isUserGeoblocked } from "utils/geolocation"; import { headers } from "next/headers"; import { @@ -12,8 +11,7 @@ import { import { symbolBytesToEmojis } from "@sdk/emoji_data"; import { MARKETS_PER_PAGE } from "lib/queries/sorting/const"; -export const revalidate = REVALIDATION_TIME; -export const dynamic = "force-dynamic"; +export const revalidate = 2; export default async function Home({ searchParams }: HomePageParams) { const { page, sortBy, orderBy, q } = toHomePageParamsWithDefault(searchParams); @@ -30,6 +28,7 @@ export default async function Home({ searchParams }: HomePageParams) { }); const priceFeed = await fetchPriceFeed({}); + // Call this last because `headers()` is a dynamic API and all fetches after this aren't cached. const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); return ( ; +} diff --git a/src/typescript/frontend/src/app/launch/page.tsx b/src/typescript/frontend/src/app/launch/page.tsx index 8675917de..fac208f2c 100644 --- a/src/typescript/frontend/src/app/launch/page.tsx +++ b/src/typescript/frontend/src/app/launch/page.tsx @@ -1,13 +1,9 @@ -import { REVALIDATION_TIME } from "lib/server-env"; import ClientLaunchEmojicoinPage from "../../components/pages/launch-emojicoin/ClientLaunchEmojicoinPage"; import { isUserGeoblocked } from "utils/geolocation"; import { headers } from "next/headers"; import { type Metadata } from "next"; import { emoji } from "utils"; -export const revalidate = REVALIDATION_TIME; -export const dynamic = "force-static"; - export const metadata: Metadata = { title: "launch", description: `Launch your own emojicoins using emojicoin.fun ${emoji("party popper")}`, diff --git a/src/typescript/frontend/src/app/market/[market]/loading.tsx b/src/typescript/frontend/src/app/market/[market]/loading.tsx new file mode 100644 index 000000000..70189e4ce --- /dev/null +++ b/src/typescript/frontend/src/app/market/[market]/loading.tsx @@ -0,0 +1,9 @@ +"use client"; + +import React from "react"; + +import LoadingComponent from "components/loading"; + +export default function Loading() { + return ; +} diff --git a/src/typescript/frontend/src/app/market/[market]/page.tsx b/src/typescript/frontend/src/app/market/[market]/page.tsx index 0a71e5dc9..d998e2e8b 100644 --- a/src/typescript/frontend/src/app/market/[market]/page.tsx +++ b/src/typescript/frontend/src/app/market/[market]/page.tsx @@ -1,6 +1,5 @@ import ClientEmojicoinPage from "components/pages/emojicoin/ClientEmojicoinPage"; import EmojiNotFoundPage from "./not-found"; -import { REVALIDATION_TIME } from "lib/server-env"; import { fetchContractMarketView } from "lib/queries/aptos-client/market-view"; import { SYMBOL_EMOJI_DATA } from "@sdk/emoji_data"; import { pathToEmojiNames } from "utils/pathname-helpers"; @@ -10,8 +9,7 @@ import { fetchChatEvents, fetchMarketState, fetchSwapEvents } from "@/queries/ma import { deriveEmojicoinPublisherAddress } from "@sdk/emojicoin_dot_fun"; import { type Metadata } from "next"; -export const revalidate = REVALIDATION_TIME; -export const dynamic = "force-dynamic"; +export const revalidate = 2; /** * Our queries work with the marketID, but the URL uses the emoji bytes with a URL encoding. @@ -72,14 +70,15 @@ const EmojicoinPage = async (params: EmojicoinPageProps) => { }); const state = await fetchMarketState({ searchEmojis: emojis }); - const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); - if (state) { const { marketID } = state.market; const marketAddress = deriveEmojicoinPublisherAddress({ emojis }); const chats = await fetchChatEvents({ marketID }); const swaps = await fetchSwapEvents({ marketID }); const marketView = await fetchContractMarketView(marketAddress.toString()); + + // Call this last because `headers()` is a dynamic API and all fetches after this aren't cached. + const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); return ( e.emoji) : undefined; - - const numRegisteredMarkets = await fetchNumRegisteredMarkets(); - const featured = await fetchFeaturedMarket(); - const markets = await fetchMarkets({ - page, - sortBy, - orderBy, - searchEmojis, - }); - const priceFeed = await fetchPriceFeed({}); - - const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); - - return ( - - ); +export default function Home() { + return ; } diff --git a/src/typescript/frontend/src/app/pools/api/getPoolDataQuery.ts b/src/typescript/frontend/src/app/pools/api/getPoolDataQuery.ts index 4402883ce..f85d8d7c2 100644 --- a/src/typescript/frontend/src/app/pools/api/getPoolDataQuery.ts +++ b/src/typescript/frontend/src/app/pools/api/getPoolDataQuery.ts @@ -3,6 +3,7 @@ import { fetchUserLiquidityPools } from "@/queries/pools"; import type { SortMarketsBy } from "@sdk/indexer-v2/types/common"; import { toOrderBy } from "@sdk/queries"; import { MARKETS_PER_PAGE } from "lib/queries/sorting/const"; +import { stringifyJSON } from "utils"; export async function getPoolData( page: number, @@ -11,23 +12,23 @@ export async function getPoolData( searchEmojis?: string[], provider?: string ) { - if (provider) { - return fetchUserLiquidityPools({ - page, - orderBy: toOrderBy(orderBy), - sortBy, - provider, - searchEmojis, - pageSize: MARKETS_PER_PAGE, - }); - } else { - return fetchMarkets({ - page, - inBondingCurve: false, - orderBy: toOrderBy(orderBy), - sortBy, - searchEmojis, - pageSize: MARKETS_PER_PAGE, - }); - } + const res = provider + ? fetchUserLiquidityPools({ + page, + orderBy: toOrderBy(orderBy), + sortBy, + provider, + searchEmojis, + pageSize: MARKETS_PER_PAGE, + }) + : fetchMarkets({ + page, + inBondingCurve: false, + orderBy: toOrderBy(orderBy), + sortBy, + searchEmojis, + pageSize: MARKETS_PER_PAGE, + }); + + return stringifyJSON(await res); } diff --git a/src/typescript/frontend/src/app/pools/api/route.ts b/src/typescript/frontend/src/app/pools/api/route.ts index 8a640addf..2268c8529 100644 --- a/src/typescript/frontend/src/app/pools/api/route.ts +++ b/src/typescript/frontend/src/app/pools/api/route.ts @@ -1,12 +1,10 @@ import { symbolBytesToEmojis } from "@sdk/emoji_data/utils"; import { getValidSortByForPoolsPage } from "@sdk/indexer-v2/queries/query-params"; import { handleEmptySearchBytes, safeParsePageWithDefault } from "lib/routes/home-page-params"; -import { stringifyJSON } from "utils"; -import { REVALIDATION_TIME } from "lib/server-env"; import { getPoolData } from "./getPoolDataQuery"; +import { unstable_cache } from "next/cache"; -export const revalidate = REVALIDATION_TIME; -export const dynamic = "force-dynamic"; +const getCachedPoolData = unstable_cache(getPoolData, ["pool-data"], { revalidate: 5 }); export async function GET(request: Request) { const { searchParams } = new URL(request.url); @@ -23,12 +21,12 @@ export async function GET(request: Request) { // The liquidity `provider`, aka the account to search for in the user liquidity pools. const provider = searchParams.get("account"); - let res: Awaited> = []; + let res: Awaited> = "[]"; try { - res = await getPoolData(page, sortBy, orderBy, searchEmojis, provider ?? undefined); + res = await getCachedPoolData(page, sortBy, orderBy, searchEmojis, provider ?? undefined); } catch (e) { console.error(e); } - return new Response(stringifyJSON(res)); + return new Response(res); } diff --git a/src/typescript/frontend/src/app/pools/loading.tsx b/src/typescript/frontend/src/app/pools/loading.tsx new file mode 100644 index 000000000..70189e4ce --- /dev/null +++ b/src/typescript/frontend/src/app/pools/loading.tsx @@ -0,0 +1,9 @@ +"use client"; + +import React from "react"; + +import LoadingComponent from "components/loading"; + +export default function Loading() { + return ; +} diff --git a/src/typescript/frontend/src/app/pools/page.tsx b/src/typescript/frontend/src/app/pools/page.tsx index f1eb01115..566dc2dc8 100644 --- a/src/typescript/frontend/src/app/pools/page.tsx +++ b/src/typescript/frontend/src/app/pools/page.tsx @@ -1,15 +1,13 @@ -import ClientPoolsPage from "components/pages/pools/ClientPoolsPage"; -import { REVALIDATION_TIME } from "lib/server-env"; +import ClientPoolsPage, { type PoolsData } from "components/pages/pools/ClientPoolsPage"; import { headers } from "next/headers"; import { isUserGeoblocked } from "utils/geolocation"; import { getPoolData } from "./api/getPoolDataQuery"; import { SortMarketsBy } from "@sdk/indexer-v2/types/common"; import { symbolBytesToEmojis } from "@sdk/emoji_data/utils"; import { type Metadata } from "next"; -import { emoji } from "utils"; +import { emoji, parseJSON } from "utils"; -export const revalidate = REVALIDATION_TIME; -export const dynamic = "force-dynamic"; +export const revalidate = 2; export const metadata: Metadata = { title: "pools", @@ -17,14 +15,18 @@ export const metadata: Metadata = { }; export default async function PoolsPage({ searchParams }: { searchParams: { pool: string } }) { - const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); - const initialData = await getPoolData( - 1, - SortMarketsBy.AllTimeVolume, - "desc", - searchParams.pool - ? symbolBytesToEmojis(searchParams.pool).emojis.map((e) => e.emoji) - : undefined + const initialData: PoolsData[] = parseJSON( + await getPoolData( + 1, + SortMarketsBy.AllTimeVolume, + "desc", + searchParams.pool + ? symbolBytesToEmojis(searchParams.pool).emojis.map((e) => e.emoji) + : undefined + ) ); + + // Call this last because `headers()` is a dynamic API and all fetches after this aren't cached. + const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); return ; } diff --git a/src/typescript/frontend/src/app/verify/loading.tsx b/src/typescript/frontend/src/app/verify/loading.tsx new file mode 100644 index 000000000..70189e4ce --- /dev/null +++ b/src/typescript/frontend/src/app/verify/loading.tsx @@ -0,0 +1,9 @@ +"use client"; + +import React from "react"; + +import LoadingComponent from "components/loading"; + +export default function Loading() { + return ; +} diff --git a/src/typescript/frontend/src/components/charts/PrivateChart.tsx b/src/typescript/frontend/src/components/charts/PrivateChart.tsx index a1782d568..61fc59279 100644 --- a/src/typescript/frontend/src/components/charts/PrivateChart.tsx +++ b/src/typescript/frontend/src/components/charts/PrivateChart.tsx @@ -30,17 +30,17 @@ import { useEventStore } from "context/event-store-context"; import { getPeriodStartTimeFromTime } from "@sdk/utils"; import { getAptosConfig } from "lib/utils/aptos-client"; import { getSymbolEmojisInString, symbolToEmojis, toMarketEmojiData } from "@sdk/emoji_data"; -import { type MarketMetadataModel } from "@sdk/indexer-v2/types"; +import { type PeriodicStateEventModel, type MarketMetadataModel } from "@sdk/indexer-v2/types"; import { getMarketResource } from "@sdk/markets"; import { Aptos } from "@aptos-labs/ts-sdk"; -import { periodEnumToRawDuration, Trigger } from "@sdk/const"; -import { fetchAllCandlesticksInTimeRange } from "@/queries/candlesticks"; +import { PeriodDuration, periodEnumToRawDuration, Trigger } from "@sdk/const"; import { type LatestBar, marketToLatestBars, periodicStateTrackerToLatestBar, toBar, } from "@/store/event/candlestick-bars"; +import { parseJSON } from "utils"; const configurationData: DatafeedConfiguration = { supported_resolutions: TV_CHARTING_LIBRARY_RESOLUTIONS, @@ -145,22 +145,31 @@ export const Chart = (props: ChartContainerProps) => { setTimeout(() => onSymbolResolvedCallback(symbolInfo), 0); }, - getBars: async (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => { + getBars: async ( + _symbolInfo, + resolution, + periodParams, + onHistoryCallback, + onErrorCallback + ) => { const { from, to } = periodParams; try { const period = ResolutionStringToPeriod[resolution.toString()]; const periodDuration = periodEnumToRawDuration(period); - const start = new Date(from * 1000); + const periodDurationSeconds = (periodDuration / PeriodDuration.PERIOD_1M) * 60; const end = new Date(to * 1000); - // TODO: Consider that if our data is internally consistent and we run into performance/scalability issues - // with this implementation below (fetching without regard for anything in state), we can store the values in - // state and coalesce that with the data we fetch from the server. - const data = await fetchAllCandlesticksInTimeRange({ + // The start timestamp is rounded so that all the people who load the webpage at a similar time get served + // the same cached response. + const start = from - (from % periodDurationSeconds); + const params = new URLSearchParams({ marketID: props.marketID, - start, - end, - period, + start: start.toString(), + period: period.toString(), + limit: "500", }); + const data: PeriodicStateEventModel[] = await fetch(`/candlesticks?${params.toString()}`) + .then((res) => res.text()) + .then((res) => parseJSON(res)); const isFetchForMostRecentBars = end.getTime() - new Date().getTime() > 1000; @@ -251,7 +260,7 @@ export const Chart = (props: ChartContainerProps) => { const time = BigInt(new Date().getTime()) * 1000n; const timeAsPeriod = getPeriodStartTimeFromTime(time, periodDuration) / 1000n; bars.push({ - time: Number(timeAsPeriod), + time: Number(timeAsPeriod.toString()), open: 0, high: 0, low: 0, diff --git a/src/typescript/frontend/src/components/loading.tsx b/src/typescript/frontend/src/components/loading.tsx index 21385fde9..7112c916e 100644 --- a/src/typescript/frontend/src/components/loading.tsx +++ b/src/typescript/frontend/src/components/loading.tsx @@ -1,3 +1,5 @@ +"use client"; + import React, { useEffect } from "react"; import AnimatedStatusIndicator, { type StaggerSpeed, diff --git a/src/typescript/frontend/src/middleware.ts b/src/typescript/frontend/src/middleware.ts index 8b74781a6..8c3023ed2 100644 --- a/src/typescript/frontend/src/middleware.ts +++ b/src/typescript/frontend/src/middleware.ts @@ -21,6 +21,10 @@ export default async function middleware(request: NextRequest) { return NextResponse.next(); } + if (!IS_ALLOWLIST_ENABLED) { + return NextResponse.next(); + } + const possibleMarketPath = normalizePossibleMarketPath(pathname, request.url); if (possibleMarketPath) { return NextResponse.redirect(possibleMarketPath); diff --git a/src/typescript/sdk/src/indexer-v2/types/json-types.ts b/src/typescript/sdk/src/indexer-v2/types/json-types.ts index 1f53c292f..5ba06dc43 100644 --- a/src/typescript/sdk/src/indexer-v2/types/json-types.ts +++ b/src/typescript/sdk/src/indexer-v2/types/json-types.ts @@ -10,7 +10,7 @@ import type { } from "../../emojicoin_dot_fun/types"; import { type Flatten } from "../../types"; -type PeriodTypeFromDatabase = +export type PeriodTypeFromDatabase = | "period_1m" | "period_5m" | "period_15m" From 560f9e457d89b2e20647d8f838a217367161e54e Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:01:42 -0800 Subject: [PATCH 19/94] [ECO-2386] Remove `--verbose` flag from pre-commit, update config to mitigate flaky tests (#333) --- .github/workflows/pre-commit.yaml | 2 +- src/typescript/frontend/.eslintrc.js | 3 ++- .../frontend/{playwright.config.ts => playwright.config.js} | 0 src/typescript/frontend/tests/tsconfig.json | 3 +-- src/typescript/sdk/tests/unit/sleep.test.ts | 2 ++ 5 files changed, 6 insertions(+), 4 deletions(-) rename src/typescript/frontend/{playwright.config.ts => playwright.config.js} (100%) diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 6a9f76ed7..d1e19c587 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -61,7 +61,7 @@ jobs: path: './src/python/hooks/.venv' - uses: 'pre-commit/action@v3.0.0' with: - extra_args: '--all-files --config cfg/pre-commit-config.yaml --verbose' + extra_args: '--all-files --config cfg/pre-commit-config.yaml' name: 'pre-commit' 'on': pull_request: null diff --git a/src/typescript/frontend/.eslintrc.js b/src/typescript/frontend/.eslintrc.js index 6a41e681a..f133c9fa8 100644 --- a/src/typescript/frontend/.eslintrc.js +++ b/src/typescript/frontend/.eslintrc.js @@ -22,7 +22,8 @@ module.exports = { "node_modules/**", ".eslintrc.js", "config-overrides.js", - "playwright.config.ts", + "next.config.mjs", + "playwright.config.js", "postcss.config.js", "tailwind.config.js", "tests/**", diff --git a/src/typescript/frontend/playwright.config.ts b/src/typescript/frontend/playwright.config.js similarity index 100% rename from src/typescript/frontend/playwright.config.ts rename to src/typescript/frontend/playwright.config.js diff --git a/src/typescript/frontend/tests/tsconfig.json b/src/typescript/frontend/tests/tsconfig.json index c30c5f46b..105efec3f 100644 --- a/src/typescript/frontend/tests/tsconfig.json +++ b/src/typescript/frontend/tests/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../tsconfig.json", "include": [ - "**/*.ts", - "../playwright.config.ts" + "**/*.ts" ] } diff --git a/src/typescript/sdk/tests/unit/sleep.test.ts b/src/typescript/sdk/tests/unit/sleep.test.ts index 1ac48e344..8066adee4 100644 --- a/src/typescript/sdk/tests/unit/sleep.test.ts +++ b/src/typescript/sdk/tests/unit/sleep.test.ts @@ -1,5 +1,7 @@ import { getTime, sleep, UNIT_OF_TIME_MULTIPLIERS, UnitOfTime } from "../../src"; +jest.retryTimes(3); + describe("sleep utility function with units of time", () => { it("converts units of time to milliseconds", () => { expect(UNIT_OF_TIME_MULTIPLIERS[UnitOfTime.Microseconds]).toEqual(0.001); From 1bdffab68045d5b0a6a53ff80b65a5b9ed0c2cc5 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Fri, 8 Nov 2024 07:06:39 +0100 Subject: [PATCH 20/94] [ECO-2354] Add slippage and gas on trading page (#317) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- src/typescript/frontend/src/app/global.css | 30 +++++++ .../components/trade-emojicoin/SwapButton.tsx | 15 +++- .../trade-emojicoin/SwapComponent.tsx | 55 +++++++++++- .../hooks/use-register-market.ts | 2 +- .../selects/dropdown-menu/index.tsx | 1 - .../selects/dropdown-menu/module.css | 29 ------ .../selects/trade-options/index.tsx | 89 +++++++++++++++++++ .../frontend/src/components/selects/types.ts | 4 + src/typescript/frontend/src/const.ts | 1 + .../lib/hooks/queries/use-simulate-swap.ts | 65 ++++++++++++-- src/typescript/frontend/src/utils/slippage.ts | 40 +++++++++ .../emojicoin_dot_fun/emojicoin-dot-fun.ts | 37 +++++++- 12 files changed, 324 insertions(+), 44 deletions(-) delete mode 100644 src/typescript/frontend/src/components/selects/dropdown-menu/module.css create mode 100644 src/typescript/frontend/src/components/selects/trade-options/index.tsx create mode 100644 src/typescript/frontend/src/utils/slippage.ts diff --git a/src/typescript/frontend/src/app/global.css b/src/typescript/frontend/src/app/global.css index 8e33c9493..1a915191d 100644 --- a/src/typescript/frontend/src/app/global.css +++ b/src/typescript/frontend/src/app/global.css @@ -27,6 +27,36 @@ --error: #f3263e; } +.med-pixel-text { + font-size: 20px !important; + line-height: 25px; + font-family: var(--font-pixelar) !important; +} + +@media screen and (min-width: 768px) { + .med-pixel-text { + font-size: 22px !important; + line-height: 27px; + font-family: var(--font-pixelar) !important; + } +} + +@media screen and (min-width: 1024px) { + .med-pixel-text { + font-size: 24px !important; + line-height: 30px; + font-family: var(--font-pixelar) !important; + } +} + +@media screen and (min-width: 1440px) { + .med-pixel-text { + font-size: 32px !important; + line-height: 40px; + font-family: var(--font-pixelar) !important; + } +} + .med-pixel-search { width: 18px; } diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapButton.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapButton.tsx index a9ff9d4d5..0c95cc109 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapButton.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapButton.tsx @@ -28,6 +28,7 @@ export const SwapButton = ({ disabled, geoblocked, symbol, + minOutputAmount, }: { inputAmount: bigint | number | string; isSell: boolean; @@ -36,6 +37,7 @@ export const SwapButton = ({ disabled?: boolean; geoblocked: boolean; symbol: string; + minOutputAmount: bigint | number | string; }) => { const { t } = translationFunction(); const { aptos, account, submit } = useAptos(); @@ -55,7 +57,7 @@ export const SwapButton = ({ inputAmount: BigInt(inputAmount), isSell, typeTags: [emojicoin, emojicoinLP], - minOutputAmount: 1n, + minOutputAmount: BigInt(minOutputAmount), }); const res = await submit(builderLambda); if (res && res.response && isUserTransactionResponse(res.response)) { @@ -81,7 +83,16 @@ export const SwapButton = ({ ); } } - }, [account, aptos.config, inputAmount, isSell, marketAddress, submit, controls]); + }, [ + account, + aptos.config, + inputAmount, + isSell, + marketAddress, + submit, + controls, + minOutputAmount, + ]); useEffect(() => { setSubmit(() => handleClick); diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx index 66d42b186..4df0a192c 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx @@ -16,7 +16,7 @@ import { toActualCoinDecimals, toDisplayCoinDecimals } from "lib/utils/decimals" import { useScramble } from "use-scramble"; import { useSimulateSwap } from "lib/hooks/queries/use-simulate-swap"; import { useEventStore } from "context/event-store-context"; -import { useMatchBreakpoints } from "@hooks/index"; +import { useMatchBreakpoints, useTooltip } from "@hooks/index"; import { useSearchParams } from "next/navigation"; import { translationFunction } from "context/language-context"; import { useAptos } from "context/wallet-context/AptosContextProvider"; @@ -26,6 +26,11 @@ import { Flex, FlexGap } from "@containers"; import Popup from "components/popup"; import { Text } from "components/text"; import { InputNumeric } from "components/inputs"; +import { emoji } from "utils"; +import { getTooltipStyles } from "components/selects/theme"; +import { useThemeContext } from "context"; +import { TradeOptions } from "components/selects/trade-options"; +import { getMaxSlippageSettings } from "utils/slippage"; const SmallButton = ({ emoji, @@ -113,6 +118,13 @@ export default function SwapComponent({ [aptBalance] ); + const [maxSlippage, setMaxSlippage] = useState(getMaxSlippageSettings().maxSlippage); + + const minOutputAmount = + outputAmount - (outputAmount * maxSlippage) / 10000n > 0n + ? outputAmount - (outputAmount * maxSlippage) / 10000n + : 1n; + const numSwaps = useEventStore( (s) => s.getMarket(marketEmojis)?.swapEvents.length ?? initNumSwaps ); @@ -122,7 +134,7 @@ export default function SwapComponent({ setEmojicoinType(emojicoinType); }, [marketAddress, setEmojicoinType]); - const swapResult = useSimulateSwap({ + const swapData = useSimulateSwap({ marketAddress, inputAmount: inputAmount.toString(), isSell, @@ -134,6 +146,14 @@ export default function SwapComponent({ decimals: OUTPUT_DISPLAY_DECIMALS, }); + let swapResult: bigint = 0n; + let gasCost: bigint | null = null; + + if (swapData) { + swapResult = swapData.swapResult; + gasCost = swapData.gasCost; + } + const { ref, replay } = useScramble({ text: new Intl.NumberFormat().format(Number(outputAmountString)), overdrive: false, @@ -189,6 +209,19 @@ export default function SwapComponent({ ); }, [t, account, isSell, aptBalance, emojicoinBalance, sufficientBalance]); + const { theme } = useThemeContext(); + + const { targetRef, tooltip } = useTooltip( + setMaxSlippage(getMaxSlippageSettings().maxSlippage)} + />, + { + placement: "bottom", + customStyles: getTooltipStyles(theme), + trigger: "click", + } + ); + return ( <> @@ -289,6 +322,23 @@ export default function SwapComponent({ {isSell ? : } +
+
+ {emoji("gear")} +
+ {tooltip} +
+ + {gasCost === null ? "~" : ""} + {toDisplayCoinDecimals({ + num: gasCost !== null ? gasCost.toString() : SWAP_GAS_COST.toString(), + decimals: 4, + })}{" "} + APT + {" "} + {emoji("fuel pump")} +
+
diff --git a/src/typescript/frontend/src/components/pages/launch-emojicoin/hooks/use-register-market.ts b/src/typescript/frontend/src/components/pages/launch-emojicoin/hooks/use-register-market.ts index 0f9dded4e..b00ab22f8 100644 --- a/src/typescript/frontend/src/components/pages/launch-emojicoin/hooks/use-register-market.ts +++ b/src/typescript/frontend/src/components/pages/launch-emojicoin/hooks/use-register-market.ts @@ -18,7 +18,7 @@ import { useNumMarkets } from "lib/hooks/queries/use-num-markets"; import { useQuery } from "@tanstack/react-query"; import { type AccountInfo } from "@aptos-labs/wallet-adapter-core"; -const tryEd25519PublicKey = (account: AccountInfo) => { +export const tryEd25519PublicKey = (account: AccountInfo) => { try { return new Ed25519PublicKey( typeof account.publicKey === "string" ? account.publicKey : account.publicKey[0] diff --git a/src/typescript/frontend/src/components/selects/dropdown-menu/index.tsx b/src/typescript/frontend/src/components/selects/dropdown-menu/index.tsx index 6e19cb79f..64af9313e 100644 --- a/src/typescript/frontend/src/components/selects/dropdown-menu/index.tsx +++ b/src/typescript/frontend/src/components/selects/dropdown-menu/index.tsx @@ -3,7 +3,6 @@ import { DropdownMenuWrapper } from "./styled"; import { DropdownMenuItem } from "./components"; import { type DropdownMenuProps } from "../types"; import { DropdownMenuInner, StyledDropdownMenuClose } from "./components/dropdown-menu-item/styled"; -import "./module.css"; export const DropdownMenu: React.FC = ({ onClick, diff --git a/src/typescript/frontend/src/components/selects/dropdown-menu/module.css b/src/typescript/frontend/src/components/selects/dropdown-menu/module.css deleted file mode 100644 index 5dda2bc73..000000000 --- a/src/typescript/frontend/src/components/selects/dropdown-menu/module.css +++ /dev/null @@ -1,29 +0,0 @@ -.med-pixel-text { - font-size: 20px !important; - line-height: 25px; - font-family: var(--font-pixelar) !important; -} - -@media screen and (min-width: 768px) { - .med-pixel-text { - font-size: 22px !important; - line-height: 27px; - font-family: var(--font-pixelar) !important; - } -} - -@media screen and (min-width: 1024px) { - .med-pixel-text { - font-size: 24px !important; - line-height: 30px; - font-family: var(--font-pixelar) !important; - } -} - -@media screen and (min-width: 1440px) { - .med-pixel-text { - font-size: 32px !important; - line-height: 40px; - font-family: var(--font-pixelar) !important; - } -} diff --git a/src/typescript/frontend/src/components/selects/trade-options/index.tsx b/src/typescript/frontend/src/components/selects/trade-options/index.tsx new file mode 100644 index 000000000..71aaf2d97 --- /dev/null +++ b/src/typescript/frontend/src/components/selects/trade-options/index.tsx @@ -0,0 +1,89 @@ +import React, { useEffect, useState } from "react"; + +import { DropdownMenuWrapper } from "../dropdown-menu/styled"; + +import { type TradeOptionsProps } from "../types"; +import { + DropdownMenuInner, + StyledDropdownMenuItem, +} from "../dropdown-menu//components/dropdown-menu-item/styled"; +import { InputNumeric } from "components/inputs"; +import { DEFAULT_MAX_SLIPPAGE } from "const"; + +import * as SlippageSettings from "../../../utils/slippage"; + +export const TradeOptions = ({ onMaxSlippageUpdate }: TradeOptionsProps) => { + const [maxSlippage, setMaxSlippage] = useState(DEFAULT_MAX_SLIPPAGE); + const [maxSlippageMode, setMaxSlippageMode] = useState("auto"); + useEffect(() => { + const { mode, maxSlippage } = SlippageSettings.getMaxSlippageSettings(); + setMaxSlippage(maxSlippage); + setMaxSlippageMode(mode); + }, []); + return ( + + + +
+ MAX. SLIPPAGE + {Number(maxSlippage) / 100}% +
+
+
+ + +
+
+ { + setMaxSlippageMode("auto"); + SlippageSettings.setMaxSlippageMode("auto"); + setMaxSlippage(SlippageSettings.getMaxSlippageSettings().maxSlippage); + if (onMaxSlippageUpdate) onMaxSlippageUpdate(); + }} + > + AUTO + + { + setMaxSlippageMode("custom"); + SlippageSettings.setMaxSlippageMode("custom"); + if (onMaxSlippageUpdate) onMaxSlippageUpdate(); + }} + > + CUSTOM + +
+
+ { + setMaxSlippage(v); + SlippageSettings.setMaxSlippage(v); + if (onMaxSlippageUpdate) onMaxSlippageUpdate(); + }} + decimals={2} + className="w-[4rem] bg-transparent text-right outline-none" + /> + % +
+
+
+
+
+ ); +}; diff --git a/src/typescript/frontend/src/components/selects/types.ts b/src/typescript/frontend/src/components/selects/types.ts index ca768e25f..0a34f4ceb 100644 --- a/src/typescript/frontend/src/components/selects/types.ts +++ b/src/typescript/frontend/src/components/selects/types.ts @@ -38,6 +38,10 @@ export type SelectProps = { tooltip: JSX.Element; }; +export interface TradeOptionsProps extends BoxProps { + onMaxSlippageUpdate?: () => void; +} + export interface DropdownMenuProps extends Omit { value?: Option | null; options: Option[]; diff --git a/src/typescript/frontend/src/const.ts b/src/typescript/frontend/src/const.ts index b9186d1d7..a493ab209 100644 --- a/src/typescript/frontend/src/const.ts +++ b/src/typescript/frontend/src/const.ts @@ -6,3 +6,4 @@ export const DEFAULT_TOAST_CONFIG = { export const LOCALSTORAGE_EXPIRY_TIME_MS = 60 * 1000; export const REVALIDATE_TEST = 2; +export const DEFAULT_MAX_SLIPPAGE = 500n; diff --git a/src/typescript/frontend/src/lib/hooks/queries/use-simulate-swap.ts b/src/typescript/frontend/src/lib/hooks/queries/use-simulate-swap.ts index fcc9d4f93..1368a11e8 100644 --- a/src/typescript/frontend/src/lib/hooks/queries/use-simulate-swap.ts +++ b/src/typescript/frontend/src/lib/hooks/queries/use-simulate-swap.ts @@ -1,4 +1,4 @@ -import { SimulateSwap } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun"; +import { SimulateSwap, Swap } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun"; import { INTEGRATOR_ADDRESS, INTEGRATOR_FEE_RATE_BPS } from "lib/env"; import { type AnyNumber, @@ -12,22 +12,57 @@ import { withResponseError } from "./client"; import Big from "big.js"; import { useMemo } from "react"; import { toCoinTypes } from "@sdk/markets/utils"; +import { type AccountInfo } from "@aptos-labs/wallet-adapter-core"; +import { tryEd25519PublicKey } from "components/pages/launch-emojicoin/hooks/use-register-market"; +import { STRUCT_STRINGS } from "@sdk/utils"; export const simulateSwap = async (args: { aptos: Aptos; + account: AccountInfo | null; swapper: AccountAddressString; marketAddress: AccountAddressString; inputAmount: AnyNumber; isSell: boolean; + minOutputAmount: AnyNumber; typeTags: [TypeTagInput, TypeTagInput]; }) => { - return withResponseError( + if (args.account) { + const publicKey = tryEd25519PublicKey(args.account); + if (publicKey) { + const res = await Swap.simulate({ + aptosConfig: args.aptos.config, + swapper: args.swapper, + swapperPubKey: publicKey, + marketAddress: args.marketAddress, + inputAmount: args.inputAmount, + isSell: args.isSell, + integrator: INTEGRATOR_ADDRESS, + integratorFeeRateBPs: INTEGRATOR_FEE_RATE_BPS, + minOutputAmount: args.minOutputAmount, + typeTags: args.typeTags, + }); + const swapEvent = res.events.find((e) => e.type === STRUCT_STRINGS.SwapEvent)!; + return { + base_volume: swapEvent.data.base_volume, + quote_volume: swapEvent.data.quote_volume, + gas_used: res.gas_used, + gas_unit_price: res.gas_unit_price, + }; + } + } + const res = await withResponseError( SimulateSwap.view({ ...args, integrator: INTEGRATOR_ADDRESS, integratorFeeRateBPs: INTEGRATOR_FEE_RATE_BPS, }) ); + return { + base_volume: res.base_volume, + quote_volume: res.quote_volume, + gas_used: null, + gas_unit_price: null, + }; }; /** @@ -45,12 +80,13 @@ export const useSimulateSwap = (args: { const { emojicoin, emojicoinLP } = toCoinTypes(marketAddress); const { aptos, account } = useAptos(); const typeTags = [emojicoin, emojicoinLP] as [TypeTag, TypeTag]; - const { inputAmount, invalid, swapper } = useMemo(() => { + const { inputAmount, invalid, swapper, minOutputAmount } = useMemo(() => { const bigInput = Big(args.inputAmount.toString()); const inputAmount = BigInt(bigInput.toString()); return { invalid: inputAmount === 0n, inputAmount, + minOutputAmount: 1n, swapper: account?.address ? (account.address as `0x${string}`) : undefined, }; }, [args.inputAmount, account?.address]); @@ -66,20 +102,35 @@ export const useSimulateSwap = (args: { emojicoin.toString(), emojicoinLP.toString(), swapper ?? "", + minOutputAmount.toString(), ], queryFn: () => invalid || typeof swapper === "undefined" ? { quote_volume: "0", base_volume: "0", + gas_used: null, + gas_unit_price: null, } - : simulateSwap({ aptos, ...args, swapper, inputAmount, typeTags }), + : simulateSwap({ + aptos, + account, + ...args, + swapper, + inputAmount, + minOutputAmount, + typeTags, + }), staleTime: Infinity, }); return typeof data === "undefined" ? data - : isSell - ? BigInt(data.quote_volume) - : BigInt(data.base_volume); + : { + gasCost: + typeof data.gas_used === "string" && typeof data.gas_unit_price === "string" + ? BigInt(data.gas_used) * BigInt(data.gas_unit_price) + : null, + swapResult: isSell ? BigInt(data.quote_volume) : BigInt(data.base_volume), + }; }; diff --git a/src/typescript/frontend/src/utils/slippage.ts b/src/typescript/frontend/src/utils/slippage.ts new file mode 100644 index 000000000..8e85cea59 --- /dev/null +++ b/src/typescript/frontend/src/utils/slippage.ts @@ -0,0 +1,40 @@ +import { DEFAULT_MAX_SLIPPAGE } from "../const"; + +export type MaxSlippageMode = "auto" | "custom"; +export const LOCALSTORAGE_MAX_SLIPPAGE_KEY = "maxSlippage"; +export const LOCALSTORAGE_MAX_SLIPPAGE_MODE_KEY = "maxSlippageMode"; + +export const getMaxSlippageSettings = () => { + let maxSlippageModeFromLocalStorage = localStorage.getItem( + LOCALSTORAGE_MAX_SLIPPAGE_MODE_KEY + ) as MaxSlippageMode; + if (!maxSlippageModeFromLocalStorage) { + setMaxSlippageMode("auto"); + maxSlippageModeFromLocalStorage = "auto"; + } + if (maxSlippageModeFromLocalStorage === "auto") { + return { + mode: "auto" as MaxSlippageMode, + maxSlippage: DEFAULT_MAX_SLIPPAGE, + }; + } else { + const maxSlippageFromLocalStorage = localStorage.getItem(LOCALSTORAGE_MAX_SLIPPAGE_KEY); + return { + mode: "custom" as MaxSlippageMode, + maxSlippage: BigInt(maxSlippageFromLocalStorage ?? "500"), + }; + } +}; + +export const setMaxSlippageMode = (mode: MaxSlippageMode) => { + if (mode !== "auto" && mode !== "custom") return; + localStorage.setItem(LOCALSTORAGE_MAX_SLIPPAGE_MODE_KEY, mode); + if (mode === "auto") { + setMaxSlippage(DEFAULT_MAX_SLIPPAGE); + } +}; + +export const setMaxSlippage = (value: bigint) => { + if (value > 10000n || value < 0n) return; + localStorage.setItem(LOCALSTORAGE_MAX_SLIPPAGE_KEY, value.toString()); +}; diff --git a/src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts b/src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts index 3a2e47335..20f136eb6 100644 --- a/src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts +++ b/src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts @@ -18,7 +18,7 @@ import { type UserTransactionResponse, type LedgerVersionArg, SimpleTransaction, - type Ed25519PublicKey, + type PublicKey, } from "@aptos-labs/ts-sdk"; import { type Option, @@ -302,7 +302,7 @@ export class RegisterMarket extends EntryFunctionPayloadBuilder { static async getGasCost(args: { aptosConfig: AptosConfig; registrant: AccountAddressInput; // &signer - registrantPubKey: Ed25519PublicKey; + registrantPubKey: PublicKey; emojis: Array; // vector> }): Promise<{ data: { amount: number; unitPrice: number }; error: boolean }> { const { aptosConfig } = args; @@ -616,6 +616,39 @@ export class Swap extends EntryFunctionPayloadBuilder { }); return response; } + + static async simulate(args: { + aptosConfig: AptosConfig; + swapper: AccountAddressInput; // &signer + swapperPubKey: PublicKey; // &signer + marketAddress: AccountAddressInput; // address + inputAmount: Uint64; // u64 + isSell: boolean; // bool + integrator: AccountAddressInput; // address + integratorFeeRateBPs: Uint8; // u8 + minOutputAmount: Uint64; // u64 + typeTags: [TypeTagInput, TypeTagInput]; // [Emojicoin, EmojicoinLP], + feePayer?: AccountAddressInput; + options?: InputGenerateTransactionOptions; + }): Promise { + const { aptosConfig } = args; + + const aptos = new Aptos(aptosConfig); + const rawTransaction = await this.builder({ + ...args, + integrator: AccountAddress.ONE, + }).then((res) => res.rawTransactionInput.rawTransaction); + const transaction = new SimpleTransaction(rawTransaction); + const [userTransactionResponse] = await aptos.transaction.simulate.simple({ + signerPublicKey: args.swapperPubKey, + transaction, + options: { + estimateGasUnitPrice: true, + estimateMaxGasAmount: true, + }, + }); + return userTransactionResponse; + } } export type SwapWithRewardsPayloadMoveArguments = { From cca7732bdbba63bd2df5d09c4c4d9f2b04ee5d27 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Mon, 11 Nov 2024 03:29:40 +0100 Subject: [PATCH 21/94] [ECO-2379] Add log environment variables for PostgREST (#336) --- src/docker/compose.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/docker/compose.yaml b/src/docker/compose.yaml index bd7cd36a3..b16ae890f 100644 --- a/src/docker/compose.yaml +++ b/src/docker/compose.yaml @@ -56,6 +56,10 @@ services: PGRST_DB_URI: 'postgres://emojicoin:emojicoin@postgres:5432/emojicoin' PGRST_DB_ANON_ROLE: 'web_anon' PGRST_DB_MAX_ROWS: '${POSTGREST_MAX_ROWS}' + PGRST_LOG_LEVEL: 'info' + PGRST_SERVER_TIMING_ENABLED: 'true' + PGRST_DB_PLAN_ENABLED: 'true' + PGRST_SERVER_TRACE_HEADER: 'X-Request-Id' image: 'postgrest/postgrest' container_name: 'postgrest' ports: From ef9328f41439a87b22a24cedf26540335cc806dd Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Tue, 12 Nov 2024 06:20:43 +0100 Subject: [PATCH 22/94] [ECO-2383] Add pagination navigation at the top of the market grid (#337) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- src/typescript/frontend/src/app/global.css | 22 +++ .../components/buttons-block/index.tsx | 8 +- .../home/components/emoji-table/index.tsx | 152 ++++++++++-------- 3 files changed, 111 insertions(+), 71 deletions(-) diff --git a/src/typescript/frontend/src/app/global.css b/src/typescript/frontend/src/app/global.css index 1a915191d..37edd9c90 100644 --- a/src/typescript/frontend/src/app/global.css +++ b/src/typescript/frontend/src/app/global.css @@ -61,6 +61,28 @@ width: 18px; } +.med-pixel-search-arrows { + width: 11px; +} + +@media screen and (min-width: 768px) { + .med-pixel-search-arrows { + width: 12px; + } +} + +@media screen and (min-width: 1024px) { + .med-pixel-search-arrows { + width: 14px; + } +} + +@media screen and (min-width: 1440px) { + .med-pixel-search-arrows { + width: 18px; + } +} + .med-pixel-close { width: 12px; } diff --git a/src/typescript/frontend/src/components/pages/home/components/emoji-table/components/buttons-block/index.tsx b/src/typescript/frontend/src/components/pages/home/components/emoji-table/components/buttons-block/index.tsx index e0b9313b6..7455a1beb 100644 --- a/src/typescript/frontend/src/components/pages/home/components/emoji-table/components/buttons-block/index.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/emoji-table/components/buttons-block/index.tsx @@ -11,17 +11,19 @@ export type ButtonsBlockProps = { value: number; numPages: number; onChange: (page: number) => void; + className?: string; }; const ButtonsBlock: React.FC = ({ value, numPages, onChange, + className, }: ButtonsBlockProps) => { const { isMobile } = useMatchBreakpoints(); const gap = isMobile ? "12px" : "17px"; return ( - + {/* First */} onChange(1)}> @@ -49,7 +51,7 @@ const ButtonsBlock: React.FC = ({ {"{"} - + {"}"} @@ -95,7 +97,7 @@ const ButtonsBlock: React.FC = ({ {"{"} - + {"}"} diff --git a/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx b/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx index 97000d53d..48e61b471 100644 --- a/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx @@ -103,74 +103,90 @@ const EmojiTable = (props: EmojiTableProps) => { }); return ( - - - - - - - - - - - - {/* Each version of the grid must wait for the other to fully exit animate out before appearing. - This provides a smooth transition from grids of varying row lengths. */} - {markets.length > 0 ? ( - <> - - - - {shouldAnimateGrid ? ( - - ) : ( - - )} - - - - - - ) : ( -
- - - Click here to launch {emojis.join("")} ! - - -
- )} -
-
-
+ <> + + + + + + + + + + + + + {/* Each version of the grid must wait for the other to fully exit animate out before appearing. + This provides a smooth transition from grids of varying row lengths. */} + {markets.length > 0 ? ( + <> + + + + {shouldAnimateGrid ? ( + + ) : ( + + )} + + + + + + ) : ( +
+ + + Click here to launch {emojis.join("")} ! + + +
+ )} +
+
+
+ ); }; From 5fea3091e04651f7e867f154aa52e47658d437df Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Tue, 12 Nov 2024 08:46:46 +0100 Subject: [PATCH 23/94] [ECO-1955] Update loading animation with hearts (#268) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- .../frontend/src/app/loading/page.tsx | 9 +++ .../emoji-picker/ColoredBytesIndicator.tsx | 4 +- .../frontend/src/components/loading.tsx | 57 +++++++++++----- .../components/desktop-grid/index.tsx | 2 +- .../animated-emoji-circle/index.tsx | 67 +++++++++++++++++++ .../index.tsx | 4 +- .../memoized-launch/index.tsx | 18 +++-- .../pools/components/liquidity/index.tsx | 4 +- 8 files changed, 136 insertions(+), 29 deletions(-) create mode 100644 src/typescript/frontend/src/app/loading/page.tsx create mode 100644 src/typescript/frontend/src/components/pages/launch-emojicoin/animated-emoji-circle/index.tsx rename src/typescript/frontend/src/components/pages/launch-emojicoin/{animated-status-indicator => animated-loading-boxes}/index.tsx (96%) diff --git a/src/typescript/frontend/src/app/loading/page.tsx b/src/typescript/frontend/src/app/loading/page.tsx new file mode 100644 index 000000000..70189e4ce --- /dev/null +++ b/src/typescript/frontend/src/app/loading/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +import React from "react"; + +import LoadingComponent from "components/loading"; + +export default function Loading() { + return ; +} diff --git a/src/typescript/frontend/src/components/emoji-picker/ColoredBytesIndicator.tsx b/src/typescript/frontend/src/components/emoji-picker/ColoredBytesIndicator.tsx index ce96434f8..c0f95ae2b 100644 --- a/src/typescript/frontend/src/components/emoji-picker/ColoredBytesIndicator.tsx +++ b/src/typescript/frontend/src/components/emoji-picker/ColoredBytesIndicator.tsx @@ -1,7 +1,7 @@ import { sumBytes } from "@sdk/utils/sum-emoji-bytes"; import { useEmojiPicker } from "context/emoji-picker-context"; import { MAX_NUM_CHAT_EMOJIS, MAX_SYMBOL_LENGTH } from "components/pages/emoji-picker/const"; -import { AnimatedStatusIndicator } from "components/pages/launch-emojicoin/animated-status-indicator"; +import { AnimatedLoadingBoxes } from "components/pages/launch-emojicoin/animated-loading-boxes"; import { motion } from "framer-motion"; import { useEffect, useState } from "react"; @@ -52,7 +52,7 @@ export const MarketValidityIndicator = ({
Too many bytes
) : null ) : typeof registered === "undefined" ? ( - + ) : registered ? (
Already Registered
) : ( diff --git a/src/typescript/frontend/src/components/loading.tsx b/src/typescript/frontend/src/components/loading.tsx index 7112c916e..9de27272c 100644 --- a/src/typescript/frontend/src/components/loading.tsx +++ b/src/typescript/frontend/src/components/loading.tsx @@ -1,47 +1,70 @@ "use client"; +// cspell:word unpathify -import React, { useEffect } from "react"; -import AnimatedStatusIndicator, { - type StaggerSpeed, -} from "./pages/launch-emojicoin/animated-status-indicator"; -import { getRandomSymbolEmoji, type SymbolEmojiData } from "@sdk/emoji_data"; +import React, { useEffect, useMemo } from "react"; +import AnimatedStatusIndicator from "./pages/launch-emojicoin/animated-emoji-circle"; +import { getRandomSymbolEmoji, SYMBOL_EMOJI_DATA, type SymbolEmojiData } from "@sdk/emoji_data"; +import { usePathname } from "next/navigation"; +import { EMOJI_PATH_INTRA_SEGMENT_DELIMITER, ONE_SPACE } from "utils/pathname-helpers"; + +const unpathify = (pathEmojiName: string) => + SYMBOL_EMOJI_DATA.byName(pathEmojiName.replaceAll(EMOJI_PATH_INTRA_SEGMENT_DELIMITER, ONE_SPACE)); export const Loading = ({ emojis, - numSquares, - animationSpeed, + numEmojis, }: { emojis?: SymbolEmojiData[]; - numSquares?: number; - animationSpeed?: StaggerSpeed; + numEmojis?: number; }) => { - const emojiCycle = emojis ?? Array.from({ length: 20 }, getRandomSymbolEmoji); - const [{ name, emoji }, setEmoji] = React.useState(emojiCycle[0]); + const pathname = usePathname(); + // Use the emojis in the path if we're on the `market` page. + const emojisInPath = pathname + .split("/market/") + .at(1) + ?.split(";") + .map(unpathify) + .filter((e) => typeof e !== "undefined"); + + const emojiCycle = useMemo(() => { + if (emojisInPath?.length || emojis?.length) { + // Note the `emojis!` below is because TypeScript can't infer that it's defined, but it definitely is. + return emojisInPath?.length ? emojisInPath : emojis!; + } + return Array.from({ length: 20 }, getRandomSymbolEmoji); + }, [emojisInPath, emojis]); + + const [emojiName, setEmojiName] = React.useState(emojiCycle[0].name); + const [emoji, setEmoji] = React.useState(emojiCycle[0].emoji); useEffect(() => { const interval = setInterval(() => { emojiCycle.unshift(emojiCycle.pop()!); - setEmoji(emojiCycle[0]); - }, 3000); + setEmoji(emojiCycle[0].emoji); + setEmojiName(emojiCycle[0].name); + }, 420.69); return () => clearInterval(interval); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, []); + const centered = "absolute left-0 right-0 ms-auto me-auto w-fit"; + return ( <>
{emoji}
- +
diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/desktop-grid/index.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/desktop-grid/index.tsx index 165931b90..28c7a3f80 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/desktop-grid/index.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/desktop-grid/index.tsx @@ -33,7 +33,7 @@ const DesktopGrid = (props: GridProps) => { className="bg-black z-10 border-t border-solid border-t-dark-gray" > - }> + }> { + const emojis = useMemo(() => Array.from({ length: numEmojis }), [numEmojis]).map(() => + getRandomSymbolEmoji() + ); + const degrees = 360 / numEmojis; + + return ( + + {emojis.map((emoji, i) => ( +
+
+ {emoji.emoji} +
+
+ +
+
+ ))} +
+ ); +}; + +export default React.memo(AnimatedStatusIndicator); diff --git a/src/typescript/frontend/src/components/pages/launch-emojicoin/animated-status-indicator/index.tsx b/src/typescript/frontend/src/components/pages/launch-emojicoin/animated-loading-boxes/index.tsx similarity index 96% rename from src/typescript/frontend/src/components/pages/launch-emojicoin/animated-status-indicator/index.tsx rename to src/typescript/frontend/src/components/pages/launch-emojicoin/animated-loading-boxes/index.tsx index 54a3b904e..4d848fbbd 100644 --- a/src/typescript/frontend/src/components/pages/launch-emojicoin/animated-status-indicator/index.tsx +++ b/src/typescript/frontend/src/components/pages/launch-emojicoin/animated-loading-boxes/index.tsx @@ -52,7 +52,7 @@ const useStaggerAnimation = ({ return scope; }; -export const AnimatedStatusIndicator = ({ +export const AnimatedLoadingBoxes = ({ numSquares = 14, delay, speed, @@ -100,4 +100,4 @@ export const AnimatedStatusIndicator = ({ ); }; -export default React.memo(AnimatedStatusIndicator); +export default React.memo(AnimatedLoadingBoxes); diff --git a/src/typescript/frontend/src/components/pages/launch-emojicoin/memoized-launch/index.tsx b/src/typescript/frontend/src/components/pages/launch-emojicoin/memoized-launch/index.tsx index 32bc5a4a2..6ebe222bc 100644 --- a/src/typescript/frontend/src/components/pages/launch-emojicoin/memoized-launch/index.tsx +++ b/src/typescript/frontend/src/components/pages/launch-emojicoin/memoized-launch/index.tsx @@ -3,7 +3,6 @@ import { MarketValidityIndicator } from "components/emoji-picker/ColoredBytesInd import EmojiPickerWithInput from "components/emoji-picker/EmojiPickerWithInput"; import { AnimatePresence, motion } from "framer-motion"; import React, { useEffect, useMemo } from "react"; -import AnimatedStatusIndicator from "../animated-status-indicator"; import { useEmojiPicker } from "context/emoji-picker-context"; import { translationFunction } from "context/language-context"; import { useRegisterMarket } from "../hooks/use-register-market"; @@ -15,6 +14,7 @@ import { toCoinDecimalString } from "lib/utils/decimals"; import { MARKET_REGISTRATION_DEPOSIT, ONE_APT_BIGINT } from "@sdk/const"; import Info from "components/info"; import { filterBigEmojis } from "components/pages/emoji-picker/EmojiPicker"; +import { useScramble } from "use-scramble"; const labelClassName = "whitespace-nowrap body-sm md:body-lg text-light-gray uppercase font-forma"; @@ -68,6 +68,15 @@ export const MemoizedLaunchAnimation = ({ } }; + const { ref } = useScramble({ + text: "Building your emojicoin...", + overdrive: false, + overflow: true, + playOnMount: true, + scramble: 10, + tick: 9, + }); + return ( {/* Input */} @@ -198,10 +207,9 @@ export const MemoizedLaunchAnimation = ({ animate={{ opacity: 1 }} className="absolute flex flex-col justify-center items-center w-full h-full gap-6" > - Building your emojicoin... -
- -
+ + Building your emojicoin... +
)}
diff --git a/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx b/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx index 84ef50b4b..24f7d4650 100644 --- a/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx +++ b/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx @@ -23,7 +23,7 @@ import { import { Arrows } from "components/svg"; import type { EntryFunctionTransactionBuilder } from "@sdk/emojicoin_dot_fun/payload-builders"; import { useSearchParams } from "next/navigation"; -import AnimatedStatusIndicator from "components/pages/launch-emojicoin/animated-status-indicator"; +import AnimatedStatusIndicator from "components/pages/launch-emojicoin/animated-emoji-circle"; import { TypeTag } from "@aptos-labs/ts-sdk"; import Info from "components/info"; import { type AnyNumberString } from "@sdk/types/types"; @@ -98,7 +98,7 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { searchParams.get("remove") !== null ? "remove" : "add" ); - const loadingComponent = useMemo(() => , []); + const loadingComponent = useMemo(() => , []); const { aptos, From f78875de228515ac2a40fa71335003ebf6c9df27 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Tue, 12 Nov 2024 21:06:48 +0100 Subject: [PATCH 24/94] [ECO-2363] Fix page number wrong on search (#323) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- .../frontend/src/app/home/HomePage.tsx | 6 +- src/typescript/frontend/src/app/home/page.tsx | 36 +++++++--- .../components/buttons-block/index.tsx | 2 +- .../emoji-table/components/index.ts | 1 - .../home/components/emoji-table/index.tsx | 6 +- .../sdk/src/indexer-v2/queries/app/home.ts | 25 +++++-- .../sdk/src/indexer-v2/queries/utils.ts | 70 ++++++++++++------- .../sdk/src/indexer-v2/types/common.ts | 1 + 8 files changed, 100 insertions(+), 47 deletions(-) delete mode 100644 src/typescript/frontend/src/components/pages/home/components/emoji-table/components/index.ts diff --git a/src/typescript/frontend/src/app/home/HomePage.tsx b/src/typescript/frontend/src/app/home/HomePage.tsx index 122140e2c..793752202 100644 --- a/src/typescript/frontend/src/app/home/HomePage.tsx +++ b/src/typescript/frontend/src/app/home/HomePage.tsx @@ -8,7 +8,7 @@ import { type MarketDataSortByHomePage } from "lib/queries/sorting/types"; export interface HomePageProps { featured?: DatabaseModels["market_state"]; markets: Array; - numRegisteredMarkets: number; + numMarkets: number; page: number; sortBy: MarketDataSortByHomePage; searchBytes?: string; @@ -20,7 +20,7 @@ export interface HomePageProps { export default async function HomePageComponent({ featured, markets, - numRegisteredMarkets, + numMarkets, page, sortBy, searchBytes, @@ -42,7 +42,7 @@ export default async function HomePageComponent({ e.emoji) : undefined; - const numRegisteredMarkets = await fetchNumRegisteredMarkets(); const featured = await fetchFeaturedMarket(); - const markets = await fetchMarkets({ - page, - sortBy, - orderBy, - searchEmojis, - pageSize: MARKETS_PER_PAGE, - }); + let numMarkets: number; + let markets: Awaited>["rows"]; + + if (searchEmojis?.length) { + const res = await fetchMarketsWithCount({ + page, + sortBy, + orderBy, + searchEmojis, + pageSize: MARKETS_PER_PAGE, + count: true, + }); + numMarkets = res.count!; + markets = res.rows; + } else { + numMarkets = await fetchNumRegisteredMarkets(); + markets = await fetchMarkets({ + page, + sortBy, + orderBy, + searchEmojis, + pageSize: MARKETS_PER_PAGE, + }); + } + const priceFeed = await fetchPriceFeed({}); // Call this last because `headers()` is a dynamic API and all fetches after this aren't cached. @@ -34,7 +52,7 @@ export default async function Home({ searchParams }: HomePageParams) { = ({ +export const ButtonsBlock: React.FC = ({ value, numPages, onChange, diff --git a/src/typescript/frontend/src/components/pages/home/components/emoji-table/components/index.ts b/src/typescript/frontend/src/components/pages/home/components/emoji-table/components/index.ts deleted file mode 100644 index 434a3c2c4..000000000 --- a/src/typescript/frontend/src/components/pages/home/components/emoji-table/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ButtonsBlock } from "./buttons-block"; diff --git a/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx b/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx index 48e61b471..d803db9b3 100644 --- a/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo } from "react"; -import { ButtonsBlock } from "./components"; +import { ButtonsBlock } from "./components/buttons-block"; import { InnerGridContainer, SearchWrapper, @@ -42,7 +42,7 @@ const EmojiTable = (props: EmojiTableProps) => { const { markets, page, sort, pages, searchBytes } = useMemo(() => { const { markets, page, sortBy: sort } = props; - const numMarkets = Math.max(props.numRegisteredMarkets, 1); + const numMarkets = Math.max(props.numMarkets, 1); const pages = Math.ceil(numMarkets / MARKETS_PER_PAGE); const searchBytes = props.searchBytes ?? ""; return { markets, page, sort, pages, searchBytes }; @@ -85,7 +85,7 @@ const EmojiTable = (props: EmojiTableProps) => { }; useEffect(() => { - pushURL(); + pushURL({ page: 0 }); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [emojis]); diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/home.ts b/src/typescript/sdk/src/indexer-v2/queries/app/home.ts index 71df6f297..2f7a90fab 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/app/home.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/app/home.ts @@ -6,12 +6,13 @@ import { LIMIT, ORDER_BY } from "../../../queries/const"; import { SortMarketsBy, type MarketStateQueryArgs } from "../../types/common"; import { DatabaseRpc, TableName } from "../../types/json-types"; import { postgrest, toQueryArray } from "../client"; -import { getLatestProcessedEmojicoinVersion, queryHelper } from "../utils"; +import { getLatestProcessedEmojicoinVersion, queryHelper, queryHelperWithCount } from "../utils"; import { DatabaseTypeConverter } from "../../types"; import { RegistryView } from "../../../emojicoin_dot_fun/emojicoin-dot-fun"; import { getAptosClient } from "../../../utils/aptos-client"; import { toRegistryView } from "../../../types"; import { sortByWithFallback } from "../query-params"; +import { type PostgrestFilterBuilder } from "@supabase/postgrest-js"; const selectMarketStates = ({ page = 1, @@ -20,10 +21,19 @@ const selectMarketStates = ({ searchEmojis, sortBy = SortMarketsBy.MarketCap, inBondingCurve, -}: MarketStateQueryArgs) => { - let query = postgrest - .from(TableName.MarketState) - .select("*") + count, + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +}: MarketStateQueryArgs): PostgrestFilterBuilder => { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + let query: any = postgrest.from(TableName.MarketState); + + if (count === true) { + query = query.select("*", { count: "exact" }); + } else { + query = query.select("*"); + } + + query = query .order(sortByWithFallback(sortBy), orderBy) .range((page - 1) * pageSize, page * pageSize - 1); @@ -43,6 +53,11 @@ export const fetchMarkets = queryHelper( DatabaseTypeConverter[TableName.MarketState] ); +export const fetchMarketsWithCount = queryHelperWithCount( + selectMarketStates, + DatabaseTypeConverter[TableName.MarketState] +); + // The featured market is simply the current highest daily volume market. export const fetchFeaturedMarket = async () => fetchMarkets({ diff --git a/src/typescript/sdk/src/indexer-v2/queries/utils.ts b/src/typescript/sdk/src/indexer-v2/queries/utils.ts index 3547b6c10..d0311d9f8 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/utils.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/utils.ts @@ -90,12 +90,9 @@ export const waitForEmojicoinIndexer = async ( }); /** + * Return the curried version of queryHelperWithCount that extracts just the rows. * - * @param queryFn Takes in a query function that's used to be called after waiting for the indexer - * to reach a certain version. Then it extracts the row data and returns it. - * @param convert A function that converts the raw row data into the desired output, usually - * by converting it into a camelCased representation of the database row. - * @returns A curried function that applies the logic to the new query function. + * @see queryHelperWithCount */ export function queryHelper< Row extends Record, @@ -108,6 +105,16 @@ export function queryHelper< queryFn: QueryFunction, QueryArgs>, convert: (rows: Row) => OutputType ): (args: WithConfig) => Promise { + // Return the curried version of queryHelperWithCount that extracts just the rows. + return async (args) => (await queryHelperWithCount(queryFn, convert)(args)).rows; +} + +export function queryHelperSingle< + T extends TableName, + Row extends DatabaseJsonType[T], + Model extends DatabaseModels[T], + QueryArgs extends Record | undefined, +>(queryFn: (args: QueryArgs) => PostgrestBuilder, convert: (row: Row) => Model) { const query = async (args: WithConfig) => { const { minimumVersion, ...queryArgs } = args; const innerQuery = queryFn(queryArgs as QueryArgs); @@ -116,29 +123,33 @@ export function queryHelper< await waitForEmojicoinIndexer(minimumVersion); } - try { - const res = await innerQuery; - const rows = extractRows(res); - if (res.error) { - console.error("[Failed row conversion]:\n"); - throw new Error(JSON.stringify(res)); - } - return rows.map(convert); - } catch (e) { - console.error(e); - return []; - } + const res = await innerQuery; + const row = extractRow(res); + return row ? convert(row) : null; }; return query; } -export function queryHelperSingle< - T extends TableName, - Row extends DatabaseJsonType[T], - Model extends DatabaseModels[T], +/** + * + * @param queryFn Takes in a query function that's used to be called after waiting for the indexer + * to reach a certain version. Then it extracts the row data and returns it. + * @param convert A function that converts the raw row data into the desired output, usually + * by converting it into a camelCased representation of the database row. + * @returns A curried function that applies the logic to the new query function. + */ +export function queryHelperWithCount< + Row extends Record, + Result extends Row[], + RelationName, + Relationships extends TableName, QueryArgs extends Record | undefined, ->(queryFn: (args: QueryArgs) => PostgrestBuilder, convert: (row: Row) => Model) { + OutputType, +>( + queryFn: QueryFunction, QueryArgs>, + convert: (rows: Row) => OutputType +): (args: WithConfig) => Promise<{ rows: OutputType[]; count: number | null }> { const query = async (args: WithConfig) => { const { minimumVersion, ...queryArgs } = args; const innerQuery = queryFn(queryArgs as QueryArgs); @@ -147,9 +158,18 @@ export function queryHelperSingle< await waitForEmojicoinIndexer(minimumVersion); } - const res = await innerQuery; - const row = extractRow(res); - return row ? convert(row) : null; + try { + const res = await innerQuery; + const rows = extractRows(res); + if (res.error) { + console.error("[Failed row conversion]:\n"); + throw new Error(JSON.stringify(res)); + } + return { rows: rows.map(convert), count: res.count }; + } catch (e) { + console.error(e); + return { rows: [], count: null }; + } }; return query; diff --git a/src/typescript/sdk/src/indexer-v2/types/common.ts b/src/typescript/sdk/src/indexer-v2/types/common.ts index 740c7ca0b..354987aad 100644 --- a/src/typescript/sdk/src/indexer-v2/types/common.ts +++ b/src/typescript/sdk/src/indexer-v2/types/common.ts @@ -19,6 +19,7 @@ export type MarketStateQueryArgs = { orderBy?: OrderBy; searchEmojis?: string[]; inBondingCurve?: boolean; + count?: boolean; }; export type PeriodicStateEventQueryArgs = { From b551ed67d778b3ea2d22ed59a574770f458f5d00 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:28:06 -0800 Subject: [PATCH 25/94] [ECO-2390] Fix flaky sdk e2e test due to period boundary cross miscalculation (#338) --- .../calculate-periodic-boundaries-crossed.ts | 55 ++++ src/typescript/sdk/src/utils/test/index.ts | 1 + .../tests/e2e/queries/client/submit.test.ts | 290 ++++++++++-------- .../sdk/tests/unit/period-boundaries.test.ts | 118 ++++++- src/typescript/turbo.json | 4 + 5 files changed, 344 insertions(+), 124 deletions(-) create mode 100644 src/typescript/sdk/src/utils/test/calculate-periodic-boundaries-crossed.ts diff --git a/src/typescript/sdk/src/utils/test/calculate-periodic-boundaries-crossed.ts b/src/typescript/sdk/src/utils/test/calculate-periodic-boundaries-crossed.ts new file mode 100644 index 000000000..a96365989 --- /dev/null +++ b/src/typescript/sdk/src/utils/test/calculate-periodic-boundaries-crossed.ts @@ -0,0 +1,55 @@ +import { type Period, periodEnumToRawDuration, PERIODS } from "../../const"; +import { type AnyNumberString } from "../../types"; +import { getPeriodBoundary } from "../misc"; + +/** + * Calculates the number of period boundaries crossed between two times. Since this will always + * be in ascending order of period boundary size, we just return the number of boundaries, not + * which specific ones. + * + * For example, (assume both times have the same date), 01:01:13 and 01:01:59 will cross 0 period + * boundaries. + * + * But 01:01:13 and 01:02:00 will cross 1 period boundary (a 1-minute period). + * + * 01:01:13 01:05:00 will cross a 1-minute and 5-minute period boundary. + * + * 01-01-2000 11:59:59 and 01-02-2000 12:00:00 will cross all period boundaries: + * 1m, 5m, 15m, 30m, 1h, 4h, and 1d. + * + * @param startMicroseconds the number/bigint/string start time in microseconds. + * @param endMicroseconds the number/bigint/string end time in microseconds. + * @returns the number of period boundaries crossed. + * @throws if the end time is later than the start time. + */ +export const calculatePeriodBoundariesCrossed = ({ + startMicroseconds, + endMicroseconds, +}: { + startMicroseconds: AnyNumberString; + endMicroseconds: AnyNumberString; +}): number => { + const start = BigInt(startMicroseconds); + const end = BigInt(endMicroseconds); + if (start > end) { + throw new Error("End time cannot be later than start time."); + } + const periodsCrossed = PERIODS.reduce( + (acc, period) => { + // Get each period boundary of the start time; i.e., round it down to the nearest boundary. + const lowerPeriodBoundary = getPeriodBoundary(start, period); + // Add the time delta for one period boundary to the start time's lower period boundary to get + // the upper (next) period boundary. + const periodDuration = BigInt(periodEnumToRawDuration(period)); + const upperPeriodBoundary = lowerPeriodBoundary + periodDuration; + // If the end time is greater than or equal to the start time's upper period boundary, that + // period boundary has been crossed. + if (end >= upperPeriodBoundary) { + acc.add(period); + } + return acc; + }, + new Set([]) as Set + ); + return periodsCrossed.size; +}; diff --git a/src/typescript/sdk/src/utils/test/index.ts b/src/typescript/sdk/src/utils/test/index.ts index 9541e4c8b..f28febebe 100644 --- a/src/typescript/sdk/src/utils/test/index.ts +++ b/src/typescript/sdk/src/utils/test/index.ts @@ -2,3 +2,4 @@ export * from "../aptos-client"; export * from "./helpers"; export * from "./publish"; export * from "./load-priv-key"; +export * from "./calculate-periodic-boundaries-crossed"; diff --git a/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts b/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts index bc20feed6..e5c0c12d7 100644 --- a/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts +++ b/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts @@ -21,6 +21,7 @@ import { } from "@aptos-labs/ts-sdk"; import { EXACT_TRANSITION_INPUT_AMOUNT } from "../../../../src/utils/test/helpers"; import { getAptosNetwork } from "../../../../src/utils/aptos-client"; +import { calculatePeriodBoundariesCrossed } from "../../../../src/utils/test"; jest.setTimeout(15000); @@ -133,53 +134,65 @@ describe("all submission types for the emojicoin client", () => { }); it("swap buys", async () => { const [sender, emojis] = senderAndSymbols[1]; - await emojicoin.register(sender, emojis, gasOptions); const inputAmount = 7654321n; - await emojicoin.buy(sender, emojis, inputAmount).then(({ response, events, swap }) => { - const { success } = response; - const payload = response.payload as EntryFunctionPayloadResponse; - expect(success).toBe(true); - expect(payload.function).toEqual(functionNames.swap); - expect(events.chatEvents.length).toEqual(0); - expect(events.globalStateEvents.length).toEqual(0); - expect(events.liquidityEvents.length).toEqual(0); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); - expect(events.stateEvents.length).toEqual(1); - expect(events.swapEvents.length).toEqual(1); - expect(events.marketRegistrationEvents.length).toEqual(0); - expect(swap.event.inputAmount).toEqual(inputAmount); - expect(swap.event.isSell).toEqual(false); - expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); - expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); - expect(swap.event.integratorFeeRateBPs).toEqual(0); - expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(swap.model.market.trigger).toEqual(Trigger.SwapBuy); + await emojicoin.register(sender, emojis, gasOptions).then(({ registration }) => { + emojicoin.buy(sender, emojis, inputAmount).then(({ response, events, swap }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.swap); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: registration.event.time, + endMicroseconds: swap.event.time, + }) + ); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(1); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(swap.event.inputAmount).toEqual(inputAmount); + expect(swap.event.isSell).toEqual(false); + expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); + expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); + expect(swap.event.integratorFeeRateBPs).toEqual(0); + expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(swap.model.market.trigger).toEqual(Trigger.SwapBuy); + }); }); }); it("swap sells", async () => { const [sender, emojis] = senderAndSymbols[2]; const inputAmount = 7654321n; await emojicoin.register(sender, emojis, gasOptions); - await emojicoin.buy(sender, emojis, inputAmount); - await emojicoin.sell(sender, emojis, inputAmount).then(({ response, events, swap }) => { - const { success } = response; - const payload = response.payload as EntryFunctionPayloadResponse; - expect(success).toBe(true); - expect(payload.function).toEqual(functionNames.swap); - expect(events.chatEvents.length).toEqual(0); - expect(events.globalStateEvents.length).toEqual(0); - expect(events.liquidityEvents.length).toEqual(0); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); - expect(events.stateEvents.length).toEqual(1); - expect(events.swapEvents.length).toEqual(1); - expect(events.marketRegistrationEvents.length).toEqual(0); - expect(swap.event.inputAmount).toEqual(inputAmount); - expect(swap.event.isSell).toEqual(true); - expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); - expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); - expect(swap.event.integratorFeeRateBPs).toEqual(0); - expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(swap.model.market.trigger).toEqual(Trigger.SwapSell); + await emojicoin.buy(sender, emojis, inputAmount).then(({ swap: buy }) => { + emojicoin.sell(sender, emojis, inputAmount).then(({ response, events, swap: sell }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.swap); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: buy.event.time, + endMicroseconds: sell.event.time, + }) + ); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(1); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(sell.event.inputAmount).toEqual(inputAmount); + expect(sell.event.isSell).toEqual(true); + expect(sell.event.swapper).toEqual(sender.accountAddress.toString()); + expect(sell.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); + expect(sell.event.integratorFeeRateBPs).toEqual(0); + expect(sell.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(sell.model.market.trigger).toEqual(Trigger.SwapSell); + }); }); }); @@ -209,59 +222,71 @@ describe("all submission types for the emojicoin client", () => { const [sender, emojis] = senderAndSymbols[3]; const [a, b] = emojis; const expectedMessage = [a, b, b, a].join(""); - await emojicoin.register(sender, emojis, gasOptions); - await emojicoin.chat(sender, emojis, [a, b, b, a]).then(({ response, events, chat }) => { - const { success } = response; - const payload = response.payload as EntryFunctionPayloadResponse; - expect(success).toBe(true); - expect(payload.function).toEqual(functionNames.chat); - expect(events.chatEvents.length).toEqual(1); - expect(events.globalStateEvents.length).toEqual(0); - expect(events.liquidityEvents.length).toEqual(0); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); - expect(events.stateEvents.length).toEqual(1); - expect(events.swapEvents.length).toEqual(0); - expect(events.marketRegistrationEvents.length).toEqual(0); - expect(chat.event.message).toEqual(expectedMessage); - expect(chat.event.user).toEqual(sender.accountAddress.toString()); - expect(chat.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(chat.model.market.trigger).toEqual(Trigger.Chat); - }); - }); - it("provides liquidity", async () => { - const [sender, emojis] = senderAndSymbols[4]; - const inputAmount = 12386n; - await emojicoin.register(sender, emojis, gasOptions); - await emojicoin.buy(sender, emojis, EXACT_TRANSITION_INPUT_AMOUNT); - await emojicoin.liquidity - .provide(sender, emojis, inputAmount) - .then(({ response, events, liquidity }) => { + await emojicoin.register(sender, emojis, gasOptions).then(({ registration }) => { + emojicoin.chat(sender, emojis, [a, b, b, a]).then(({ response, events, chat }) => { const { success } = response; const payload = response.payload as EntryFunctionPayloadResponse; expect(success).toBe(true); - expect(payload.function).toEqual(functionNames.provideLiquidity); - expect(events.chatEvents.length).toEqual(0); + expect(payload.function).toEqual(functionNames.chat); + expect(events.chatEvents.length).toEqual(1); expect(events.globalStateEvents.length).toEqual(0); - expect(events.liquidityEvents.length).toEqual(1); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: registration.event.time, + endMicroseconds: chat.event.emitTime, + }) + ); expect(events.stateEvents.length).toEqual(1); expect(events.swapEvents.length).toEqual(0); expect(events.marketRegistrationEvents.length).toEqual(0); - expect(liquidity.event.quoteAmount).toEqual(inputAmount); - expect(liquidity.event.provider).toEqual(sender.accountAddress.toString()); - expect(liquidity.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(liquidity.model.market.trigger).toEqual(Trigger.ProvideLiquidity); + expect(chat.event.message).toEqual(expectedMessage); + expect(chat.event.user).toEqual(sender.accountAddress.toString()); + expect(chat.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(chat.model.market.trigger).toEqual(Trigger.Chat); }); + }); + }); + it("provides liquidity", async () => { + const [sender, emojis] = senderAndSymbols[4]; + const inputAmount = 12386n; + await emojicoin.register(sender, emojis, gasOptions); + await emojicoin.buy(sender, emojis, EXACT_TRANSITION_INPUT_AMOUNT).then(({ swap }) => { + emojicoin.liquidity + .provide(sender, emojis, inputAmount) + .then(({ response, events, liquidity }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.provideLiquidity); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(1); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: swap.event.time, + endMicroseconds: liquidity.event.time, + }) + ); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(0); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(liquidity.event.quoteAmount).toEqual(inputAmount); + expect(liquidity.event.provider).toEqual(sender.accountAddress.toString()); + expect(liquidity.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(liquidity.model.market.trigger).toEqual(Trigger.ProvideLiquidity); + }); + }); }); it("removes liquidity", async () => { const [sender, emojis] = senderAndSymbols[5]; await emojicoin.register(sender, emojis, gasOptions); await emojicoin.buy(sender, emojis, EXACT_TRANSITION_INPUT_AMOUNT); - await emojicoin.liquidity.provide(sender, emojis, 59182n).then(({ liquidity }) => { - const lpCoinAmount = liquidity.event.lpCoinAmount; + await emojicoin.liquidity.provide(sender, emojis, 59182n).then(({ liquidity: provide }) => { + const lpCoinAmount = provide.event.lpCoinAmount; emojicoin.liquidity .remove(sender, emojis, lpCoinAmount) - .then(({ response, events, liquidity }) => { + .then(({ response, events, liquidity: remove }) => { const { success } = response; const payload = response.payload as EntryFunctionPayloadResponse; expect(success).toBe(true); @@ -269,14 +294,19 @@ describe("all submission types for the emojicoin client", () => { expect(events.chatEvents.length).toEqual(0); expect(events.globalStateEvents.length).toEqual(0); expect(events.liquidityEvents.length).toEqual(1); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: provide.event.time, + endMicroseconds: remove.event.time, + }) + ); expect(events.stateEvents.length).toEqual(1); expect(events.swapEvents.length).toEqual(0); expect(events.marketRegistrationEvents.length).toEqual(0); - expect(liquidity.event.provider).toEqual(sender.accountAddress.toString()); - expect(liquidity.event.lpCoinAmount).toEqual(lpCoinAmount); - expect(liquidity.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(liquidity.model.market.trigger).toEqual(Trigger.RemoveLiquidity); + expect(remove.event.provider).toEqual(sender.accountAddress.toString()); + expect(remove.event.lpCoinAmount).toEqual(lpCoinAmount); + expect(remove.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(remove.model.market.trigger).toEqual(Trigger.RemoveLiquidity); }); }); }); @@ -284,52 +314,66 @@ describe("all submission types for the emojicoin client", () => { it("swap buys with the rewards contract", async () => { const [sender, emojis] = senderAndSymbols[6]; const inputAmount = 1234567n; - await emojicoin.register(sender, emojis, gasOptions); - await emojicoin.rewards.buy(sender, emojis, inputAmount).then(({ response, events, swap }) => { - const { success } = response; - const payload = response.payload as EntryFunctionPayloadResponse; - expect(success).toBe(true); - expect(payload.function).toEqual(functionNames.rewardsSwap); - expect(events.chatEvents.length).toEqual(0); - expect(events.globalStateEvents.length).toEqual(0); - expect(events.liquidityEvents.length).toEqual(0); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); - expect(events.stateEvents.length).toEqual(1); - expect(events.swapEvents.length).toEqual(1); - expect(events.marketRegistrationEvents.length).toEqual(0); - expect(swap.event.inputAmount).toEqual(inputAmount); - expect(swap.event.isSell).toEqual(false); - expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); - expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); - expect(swap.event.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); - expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(swap.model.market.trigger).toEqual(Trigger.SwapBuy); + await emojicoin.register(sender, emojis, gasOptions).then(({ registration }) => { + emojicoin.rewards.buy(sender, emojis, inputAmount).then(({ response, events, swap }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.rewardsSwap); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: registration.event.time, + endMicroseconds: swap.event.time, + }) + ); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(1); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(swap.event.inputAmount).toEqual(inputAmount); + expect(swap.event.isSell).toEqual(false); + expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); + expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); + expect(swap.event.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); + expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(swap.model.market.trigger).toEqual(Trigger.SwapBuy); + }); }); }); it("swap sells with the rewards contract", async () => { const [sender, emojis] = senderAndSymbols[7]; const inputAmount = 1234567n; await emojicoin.register(sender, emojis, gasOptions); - await emojicoin.rewards.buy(sender, emojis, inputAmount); - await emojicoin.rewards.sell(sender, emojis, inputAmount).then(({ response, events, swap }) => { - const { success } = response; - const payload = response.payload as EntryFunctionPayloadResponse; - expect(success).toBe(true); - expect(payload.function).toEqual(functionNames.rewardsSwap); - expect(events.chatEvents.length).toEqual(0); - expect(events.globalStateEvents.length).toEqual(0); - expect(events.liquidityEvents.length).toEqual(0); - expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); - expect(events.stateEvents.length).toEqual(1); - expect(events.swapEvents.length).toEqual(1); - expect(events.marketRegistrationEvents.length).toEqual(0); - expect(swap.event.inputAmount).toEqual(inputAmount); - expect(swap.event.isSell).toEqual(true); - expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); - expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); - expect(swap.event.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); - expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); - expect(swap.model.market.trigger).toEqual(Trigger.SwapSell); + await emojicoin.rewards.buy(sender, emojis, inputAmount).then(({ swap: buy }) => { + emojicoin.rewards + .sell(sender, emojis, inputAmount) + .then(({ response, events, swap: sell }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.rewardsSwap); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toEqual( + calculatePeriodBoundariesCrossed({ + startMicroseconds: buy.event.time, + endMicroseconds: sell.event.time, + }) + ); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(1); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(sell.event.inputAmount).toEqual(inputAmount); + expect(sell.event.isSell).toEqual(true); + expect(sell.event.swapper).toEqual(sender.accountAddress.toString()); + expect(sell.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); + expect(sell.event.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); + expect(sell.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(sell.model.market.trigger).toEqual(Trigger.SwapSell); + }); }); }); diff --git a/src/typescript/sdk/tests/unit/period-boundaries.test.ts b/src/typescript/sdk/tests/unit/period-boundaries.test.ts index ce2b66689..2b0939302 100644 --- a/src/typescript/sdk/tests/unit/period-boundaries.test.ts +++ b/src/typescript/sdk/tests/unit/period-boundaries.test.ts @@ -1,4 +1,5 @@ -import { PeriodDuration, getPeriodStartTime } from "../../src"; +import { PERIODS, PeriodDuration, getPeriodStartTime } from "../../src"; +import { calculatePeriodBoundariesCrossed } from "../../src/utils/test"; import { SAMPLE_STATE_EVENT, SAMPLE_SWAP_EVENT } from "../../src/utils/test/sample-data"; const swap = SAMPLE_SWAP_EVENT; @@ -123,3 +124,118 @@ describe("tests period boundaries", () => { expect(getPeriodStartTime(state, PERIOD_30M) === 0n * BigInt(PERIOD_30M)).toEqual(true); }); }); + +describe("calculates period boundaries crossed", () => { + type TwoNumbers = `${number}${number}`; + type TimeFormat = `${TwoNumbers}:${TwoNumbers}:${TwoNumbers}`; + const defaultDate = "01-01-2000"; + const getUTCDateFromHMS = (time: TimeFormat) => new Date(`${defaultDate} ${time}Z`); + // Converts hours:minutes:seconds to the number of microseconds since the Unix epoch using a + // default date for the day, month and year. + const getMicrosecondsFromHMS = (time: TimeFormat) => + BigInt(getUTCDateFromHMS(time).getTime() * 1000); + + it("uses the same date by default with the utility function", () => { + const times: Array = ["00:00:00", "11:59:59", "12:00:00", "23:59:59"]; + times.forEach((time) => { + const utcDate = getUTCDateFromHMS(time); + expect(utcDate.getUTCDate()).toEqual(1); + expect(utcDate.getUTCMonth()).toEqual(0); // getUTCMonth() is offset by 0; December is 11. + expect(utcDate.getUTCFullYear()).toEqual(2000); + const micros = getMicrosecondsFromHMS(time); + expect( + calculatePeriodBoundariesCrossed({ startMicroseconds: micros, endMicroseconds: micros }) + ).toEqual(0); + const date = new Date(Number(micros / 1000n)); + expect(date.getUTCDate()).toEqual(1); + expect(date.getUTCMonth()).toEqual(0); // getUTCMonth() is offset by 0; December is 11. + expect(date.getUTCFullYear()).toEqual(2000); + }); + }); + + it("calculates that no period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:01:13"); + const endMicroseconds = getMicrosecondsFromHMS("01:01:59"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(0); + }); + + it("calculates that a 1 minute period boundary is crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:01:13"); + const endMicroseconds = getMicrosecondsFromHMS("01:02:00"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(1); + }); + + it("calculates that 1m and 5m period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:04:59"); + const endMicroseconds = getMicrosecondsFromHMS("01:05:00"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(2); + }); + + it("calculates that 1m, 5m, and 15m period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:14:59"); + const endMicroseconds = getMicrosecondsFromHMS("01:15:00"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(3); + }); + + it("calculates that 1m, 5m, 15m, and 30m period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:29:59"); + const endMicroseconds = getMicrosecondsFromHMS("01:30:00"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(4); + }); + + it("calculates that 1m, 5m, 15m, 30m, and 1h period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:59:59"); + const endMicroseconds = getMicrosecondsFromHMS("02:00:00"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(5); + }); + + it("calculates that 1m, 5m, 15m, 30m, 1h, and 4h period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("03:59:59"); + const endMicroseconds = getMicrosecondsFromHMS("04:00:00"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(6); + }); + + it("calculates that all period boundaries are crossed", () => { + const startMicroseconds = getMicrosecondsFromHMS("23:59:59"); + const oneDay = BigInt(PeriodDuration.PERIOD_1D); + const endMicroseconds = getMicrosecondsFromHMS("00:00:00") + oneDay; + const startDay = new Date(Number(startMicroseconds / 1000n)).getUTCDate(); + const endDay = new Date(Number(endMicroseconds / 1000n)).getUTCDate(); + expect(startDay + 1).toEqual(endDay); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(PERIODS.length); + }); + + it("calculates that exactly only 1 period boundary is crossed over a 4m59s time period", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:00:00"); + const endMicroseconds = getMicrosecondsFromHMS("01:04:59"); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(1); + }); + + it("calculates that exactly 7 period boundaries are crossed over multiple days", () => { + const startMicroseconds = getMicrosecondsFromHMS("01:01:13"); + const threeDays = BigInt(PeriodDuration.PERIOD_1D) * 3n; + const endMicroseconds = getMicrosecondsFromHMS("01:02:00") + threeDays; + const startDay = new Date(Number(startMicroseconds / 1000n)).getUTCDate(); + const endDay = new Date(Number(endMicroseconds / 1000n)).getUTCDate(); + expect(startDay + 3).toEqual(endDay); + const numBoundaries = calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(numBoundaries).toEqual(7); + expect(numBoundaries).toEqual(PERIODS.length); + }); + + it("throws if the end time is later than the start time", () => { + const startMicroseconds = new Date("01-01-2000 00:00:01Z").getTime() * 1000; + const endMicroseconds = new Date("01-01-2000 00:00:00Z").getTime() * 1000; + const fn = () => calculatePeriodBoundariesCrossed({ startMicroseconds, endMicroseconds }); + expect(fn).toThrow(); + }); +}); diff --git a/src/typescript/turbo.json b/src/typescript/turbo.json index a03f5fa9f..74ce297cd 100644 --- a/src/typescript/turbo.json +++ b/src/typescript/turbo.json @@ -71,6 +71,10 @@ "test:sequential": { "cache": false, "outputs": [] + }, + "test:unit": { + "cache": false, + "outputs": [] } } } From 192f1790618b383365a119d0764cf83bda65ec06 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:07:01 -0800 Subject: [PATCH 26/94] [ECO-2391] Ignore lint/type errors when building the frontend Docker container (#340) --- src/docker/frontend/Dockerfile | 2 +- src/typescript/frontend/next.config.mjs | 1 + src/typescript/frontend/package.json | 1 + src/typescript/package.json | 1 + src/typescript/sdk/package.json | 1 + src/typescript/turbo.json | 17 ++++++++++------- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/docker/frontend/Dockerfile b/src/docker/frontend/Dockerfile index 091d2b9cd..908e99374 100644 --- a/src/docker/frontend/Dockerfile +++ b/src/docker/frontend/Dockerfile @@ -31,6 +31,6 @@ ENV HASH_SEED=$HASH_SEED \ REVALIDATION_TIME=$REVALIDATION_TIME \ EMOJICOIN_INDEXER_URL=$EMOJICOIN_INDEXER_URL -RUN ["bash", "-c", "pnpm install && pnpm run build"] +RUN ["bash", "-c", "pnpm install && pnpm run build:no-checks"] CMD ["bash", "-c", "pnpm run start -- -H 0.0.0.0"] diff --git a/src/typescript/frontend/next.config.mjs b/src/typescript/frontend/next.config.mjs index 38d24b26c..e6435a94e 100644 --- a/src/typescript/frontend/next.config.mjs +++ b/src/typescript/frontend/next.config.mjs @@ -38,6 +38,7 @@ const nextConfig = { crossOrigin: "use-credentials", typescript: { tsconfigPath: "tsconfig.json", + ignoreBuildErrors: process.env.IGNORE_BUILD_ERRORS === "true", }, compiler: { styledComponents: DEBUG ? styledComponentsConfig : true, diff --git a/src/typescript/frontend/package.json b/src/typescript/frontend/package.json index f0156999d..7e53a53d7 100644 --- a/src/typescript/frontend/package.json +++ b/src/typescript/frontend/package.json @@ -91,6 +91,7 @@ "_format": "prettier './**/*.{js,jsx,ts,tsx,css,md}' --config ./.prettierrc.js", "build": "next build", "build:debug": "BUILD_DEBUG=true next build --no-lint --no-mangling --debug", + "build:no-checks": "IGNORE_BUILD_ERRORS=true next build --no-lint", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist && rm -rf .next", "dev": "NODE_OPTIONS='--inspect' next dev --turbo --port 3001", "format": "pnpm _format --write", diff --git a/src/typescript/package.json b/src/typescript/package.json index 8215fe55f..103b52ce5 100644 --- a/src/typescript/package.json +++ b/src/typescript/package.json @@ -19,6 +19,7 @@ "scripts": { "build": "pnpm i && pnpm load-env:test -- turbo run build", "build:debug": "pnpm i && pnpm load-env:test -- turbo run build:debug", + "build:no-checks": "pnpm i && pnpm load-env:test -- turbo run build:no-checks", "check": "turbo run check", "clean": "turbo run clean --no-cache --force && rm -rf .turbo", "clean:full": "pnpm run clean && rm -rf node_modules && rm -rf sdk/node_modules && rm -rf frontend/node_modules", diff --git a/src/typescript/sdk/package.json b/src/typescript/sdk/package.json index 1fc590b2e..6c5eadadf 100644 --- a/src/typescript/sdk/package.json +++ b/src/typescript/sdk/package.json @@ -56,6 +56,7 @@ "_format": "prettier 'src/**/*.ts' 'tests/**/*.ts' '.eslintrc.js'", "build": "tsc", "build:debug": "BUILD_DEBUG=true pnpm run build", + "build:no-checks": "tsc --skipLibCheck", "check": "tsc -p tests/tsconfig.json --noEmit", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", "e2e:testnet": "pnpm load-test-env -v NO_TEST_SETUP=true -- pnpm jest tests/e2e/queries/testnet", diff --git a/src/typescript/turbo.json b/src/typescript/turbo.json index 74ce297cd..7392f70bd 100644 --- a/src/typescript/turbo.json +++ b/src/typescript/turbo.json @@ -16,10 +16,16 @@ ] }, "build:debug": { - "dependsOn": [ - "build" - ], - "outputs": [] + "outputs": [ + "dist/**", + ".next/**" + ] + }, + "build:no-checks": { + "outputs": [ + "dist/**", + ".next/**" + ] }, "check": { "outputs": [] @@ -47,9 +53,6 @@ "outputs": [] }, "start": { - "dependsOn": [ - "build" - ], "outputs": [], "persistent": true }, From d15827f02b30b86723f1d09fb3e2386a949901de Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Wed, 13 Nov 2024 02:34:08 +0100 Subject: [PATCH 27/94] [ECO-2358] Update chart error message (#331) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- .../src/components/charts/PrivateChart.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/typescript/frontend/src/components/charts/PrivateChart.tsx b/src/typescript/frontend/src/components/charts/PrivateChart.tsx index 61fc59279..520028b34 100644 --- a/src/typescript/frontend/src/components/charts/PrivateChart.tsx +++ b/src/typescript/frontend/src/components/charts/PrivateChart.tsx @@ -1,7 +1,7 @@ // cspell:word intraday // cspell:word minmov // cspell:word pricescale -import { useEffect, useMemo, useRef } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { EXCHANGE_NAME, @@ -357,11 +357,22 @@ export const Chart = (props: ChartContainerProps) => { }; }, [datafeed, symbol]); // eslint-disable-line react-hooks/exhaustive-deps + const [showErrorMessage, setShowErrorMessage] = useState(false); + + useEffect(() => { + const timeout = setTimeout(() => { + setShowErrorMessage(true); + }, 3500); + return () => clearTimeout(timeout); + }); + return (
- {"The device you're using isn't supported. 😔 Please try viewing on another device."} + {showErrorMessage + ? "The browser you're using isn't supported. 😔 Please try viewing in another browser." + : "Loading..."}
From fd55939f6a84caf19566dc4dd92596b63ead78cd Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Wed, 13 Nov 2024 04:10:55 +0100 Subject: [PATCH 28/94] [ECO-2364] Fix double rendering issue on home page (#341) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- .../pages/home/components/emoji-table/ClientGrid.tsx | 10 ++++------ .../pages/home/components/emoji-table/utils.ts | 9 ++------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/typescript/frontend/src/components/pages/home/components/emoji-table/ClientGrid.tsx b/src/typescript/frontend/src/components/pages/home/components/emoji-table/ClientGrid.tsx index cd0eb6b9a..0dd2884ef 100644 --- a/src/typescript/frontend/src/components/pages/home/components/emoji-table/ClientGrid.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/emoji-table/ClientGrid.tsx @@ -6,9 +6,8 @@ import { marketDataToProps } from "./utils"; import { useGridRowLength } from "./hooks/use-grid-items-per-line"; import { type MarketDataSortByHomePage } from "lib/queries/sorting/types"; import { useEffect, useMemo, useRef } from "react"; -import "./module.css"; -import { useEmojiPicker } from "context/emoji-picker-context"; import { type HomePageProps } from "app/home/HomePage"; +import "./module.css"; export const ClientGrid = ({ markets, @@ -20,11 +19,10 @@ export const ClientGrid = ({ sortBy: MarketDataSortByHomePage; }) => { const rowLength = useGridRowLength(); - const searchEmojis = useEmojiPicker((s) => s.emojis); const ordered = useMemo(() => { - return marketDataToProps(markets, searchEmojis); - }, [markets, searchEmojis]); + return marketDataToProps(markets); + }, [markets]); const initialRender = useRef(true); @@ -41,7 +39,7 @@ export const ClientGrid = ({ {ordered.map((v, i) => { return ( & { time: number; - searchEmojisKey: string; }; export type PropsWithTimeAndIndex = TableCardProps & { time: number; @@ -19,10 +18,7 @@ export type WithTimeIndexAndPrev = PropsWithTimeAndIndex & { const toSearchEmojisKey = (searchEmojis: string[]) => `{${searchEmojis.join("")}}`; -export const marketDataToProps = ( - markets: HomePageProps["markets"], - searchEmojis: string[] -): PropsWithTime[] => +export const marketDataToProps = (markets: HomePageProps["markets"]): PropsWithTime[] => markets.map((m) => ({ time: Number(m.market.time), symbol: m.market.symbolData.symbol, @@ -30,7 +26,6 @@ export const marketDataToProps = ( emojis: m.market.emojis, staticMarketCap: m.state.instantaneousStats.marketCap.toString(), staticVolume24H: m.dailyVolume.toString(), - searchEmojisKey: toSearchEmojisKey(searchEmojis), })); export const stateEventsToProps = ( @@ -103,7 +98,7 @@ export const constructOrdered = ({ // We don't need to filter the fetched data because it's already filtered and sorted by the // server. We only need to filter event store state events. const searchEmojis = getSearchEmojis() as Array; - const initial = marketDataToProps(markets, searchEmojis); + const initial = marketDataToProps(markets); // If we're sorting by bump order, deduplicate and sort the events by bump order. const bumps = stateEventsToProps(stateFirehose, getMarket, searchEmojis); From e72edecff77537e3e1327cfeb3b7b58591cd2a2e Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:22:43 -0800 Subject: [PATCH 29/94] [ECO-2337] Remove `production-branch-protection.yaml` rule (#344) --- .../production-branch-protection.yaml | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 .github/workflows/production-branch-protection.yaml diff --git a/.github/workflows/production-branch-protection.yaml b/.github/workflows/production-branch-protection.yaml deleted file mode 100644 index 9b808be3a..000000000 --- a/.github/workflows/production-branch-protection.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# yamllint disable rule:empty-lines rule:key-ordering ---- -name: 'Production Branch Protection' - -"on": - pull_request: - types: - - 'opened' - - 'reopened' - - 'synchronize' - - 'edited' - -jobs: - production-branch-protection: - runs-on: 'ubuntu-latest' - steps: - - name: 'Check if the PR is targeting the production branch' - id: 'check_branch' - if: | - github.base_ref == 'production' && github.head_ref != 'main' - run: | - echo ERROR: You can only merge to the production branch from main. - exit 1 -... From f36aee733c74084520bc07f5f64443f21cc57d0f Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Wed, 13 Nov 2024 05:29:08 +0100 Subject: [PATCH 30/94] [ECO-2375] Use emoji component to display emojis (#339) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- src/typescript/frontend/package.json | 2 +- .../src/components/charts/PrivateChart.tsx | 21 ++++++--- .../emoji-picker/EmojiPickerWithInput.tsx | 6 +++ .../header/wallet-button/OuterConnectText.tsx | 8 +++- .../frontend/src/components/loading.tsx | 27 +++++++++-- .../components/message-container/index.tsx | 15 ++++--- .../components/emoji-not-found/index.tsx | 4 +- .../components/main-info/MainInfo.tsx | 5 +-- .../trade-emojicoin/CongratulationsToast.tsx | 4 +- .../trade-emojicoin/InputLabels.tsx | 8 ++-- .../trade-emojicoin/SwapComponent.tsx | 36 ++++++++------- .../components/trade-history/index.tsx | 3 +- .../trade-history/table-row/index.tsx | 8 ++-- .../home/components/emoji-table/index.tsx | 3 +- .../home/components/main-card/MainCard.tsx | 28 +++++------- .../home/components/main-card/module.css | 37 ++++++++++++--- .../home/components/table-card/TableCard.tsx | 9 ++-- .../animated-emoji-circle/index.tsx | 8 ++-- .../animated-loading-boxes/index.tsx | 3 +- .../memoized-launch/index.tsx | 14 +++--- .../components/table-row-desktop/XprPopup.tsx | 4 +- .../components/table-row-desktop/index.tsx | 3 +- .../components/pools-table/constants/index.ts | 45 ------------------- .../pages/verify_status/VerifyStatusPage.tsx | 8 ++-- .../src/components/price-feed/inner.tsx | 3 +- .../components/wallet/WalletDropdownMenu.tsx | 12 ++++- .../src/context/ConnectToWebSockets.tsx | 7 ++- .../wallet-context/AptosContextProvider.tsx | 5 ++- .../src/context/wallet-context/WalletItem.tsx | 16 ++++--- .../src/lib/store/emoji-picker-store.ts | 10 ++--- .../src/lib/store/emoji-picker-utils.ts | 2 +- .../store/server-to-client/StoreOnClient.tsx | 3 +- src/typescript/frontend/src/utils/emoji.tsx | 36 +++++++++++++++ src/typescript/frontend/src/utils/index.ts | 7 ++- src/typescript/package.json | 4 +- src/typescript/sdk/package.json | 2 +- src/typescript/sdk/src/emoji_data/types.ts | 5 +++ src/typescript/sdk/src/emoji_data/utils.ts | 5 ++- 38 files changed, 264 insertions(+), 162 deletions(-) create mode 100644 src/typescript/frontend/src/utils/emoji.tsx diff --git a/src/typescript/frontend/package.json b/src/typescript/frontend/package.json index 7e53a53d7..5fe891992 100644 --- a/src/typescript/frontend/package.json +++ b/src/typescript/frontend/package.json @@ -88,7 +88,7 @@ "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a", "private": true, "scripts": { - "_format": "prettier './**/*.{js,jsx,ts,tsx,css,md}' --config ./.prettierrc.js", + "_format": "prettier -l './**/*.{js,jsx,ts,tsx,css,md}' --config ./.prettierrc.js", "build": "next build", "build:debug": "BUILD_DEBUG=true next build --no-lint --no-mangling --debug", "build:no-checks": "IGNORE_BUILD_ERRORS=true next build --no-lint", diff --git a/src/typescript/frontend/src/components/charts/PrivateChart.tsx b/src/typescript/frontend/src/components/charts/PrivateChart.tsx index 520028b34..461491f00 100644 --- a/src/typescript/frontend/src/components/charts/PrivateChart.tsx +++ b/src/typescript/frontend/src/components/charts/PrivateChart.tsx @@ -40,7 +40,8 @@ import { periodicStateTrackerToLatestBar, toBar, } from "@/store/event/candlestick-bars"; -import { parseJSON } from "utils"; +import { emoji, parseJSON } from "utils"; +import { Emoji } from "utils/emoji"; const configurationData: DatafeedConfiguration = { supported_resolutions: TV_CHARTING_LIBRARY_RESOLUTIONS, @@ -368,11 +369,21 @@ export const Chart = (props: ChartContainerProps) => { return (
-
+
- {showErrorMessage - ? "The browser you're using isn't supported. 😔 Please try viewing in another browser." - : "Loading..."} + {showErrorMessage ? ( + <> +
+ {"The browser you're using isn't supported. "} + +
+
+ {" Please try viewing in another browser."} +
+ + ) : ( + "Loading..." + )}
diff --git a/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx b/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx index 8eea83948..66a1836ed 100644 --- a/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx +++ b/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx @@ -18,6 +18,11 @@ import { createPortal } from "react-dom"; import { type EmojiMartData } from "components/pages/emoji-picker/types"; import { useWallet } from "@aptos-labs/wallet-adapter-react"; +const EMOJI_FONT_FAMILY = + '"EmojiMart", "Segoe UI Emoji", "Segoe UI Symbol", ' + + '"Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", ' + + '"Android Emoji"'; + const ChatInputBox = ({ children, geoblocked, @@ -251,6 +256,7 @@ export const EmojiPickerWithInput = ({ setPickerInvisible(false); }} data-testid="emoji-input" + style={{ fontFamily: EMOJI_FONT_FAMILY }} /> {mode === "search" && close} {mode === "chat" ? ( diff --git a/src/typescript/frontend/src/components/header/wallet-button/OuterConnectText.tsx b/src/typescript/frontend/src/components/header/wallet-button/OuterConnectText.tsx index 590db7e54..7bdd54d09 100644 --- a/src/typescript/frontend/src/components/header/wallet-button/OuterConnectText.tsx +++ b/src/typescript/frontend/src/components/header/wallet-button/OuterConnectText.tsx @@ -1,3 +1,6 @@ +import { emoji } from "utils"; +import { Emoji } from "utils/emoji"; + export const OuterConnectText = ({ side, connected, @@ -15,7 +18,10 @@ export const OuterConnectText = ({ } return (
- {"⚡"} +
); } else { diff --git a/src/typescript/frontend/src/components/loading.tsx b/src/typescript/frontend/src/components/loading.tsx index 9de27272c..0b59cf3c8 100644 --- a/src/typescript/frontend/src/components/loading.tsx +++ b/src/typescript/frontend/src/components/loading.tsx @@ -4,12 +4,22 @@ import React, { useEffect, useMemo } from "react"; import AnimatedStatusIndicator from "./pages/launch-emojicoin/animated-emoji-circle"; import { getRandomSymbolEmoji, SYMBOL_EMOJI_DATA, type SymbolEmojiData } from "@sdk/emoji_data"; +import { Emoji } from "utils/emoji"; import { usePathname } from "next/navigation"; import { EMOJI_PATH_INTRA_SEGMENT_DELIMITER, ONE_SPACE } from "utils/pathname-helpers"; +import { type EmojiMartData } from "./pages/emoji-picker/types"; +import { init } from "emoji-mart"; const unpathify = (pathEmojiName: string) => SYMBOL_EMOJI_DATA.byName(pathEmojiName.replaceAll(EMOJI_PATH_INTRA_SEGMENT_DELIMITER, ONE_SPACE)); +const data = fetch("https://cdn.jsdelivr.net/npm/@emoji-mart/data@latest/sets/15/native.json").then( + (res) => + res.json().then((data) => { + return data as EmojiMartData; + }) +); + export const Loading = ({ emojis, numEmojis, @@ -17,6 +27,16 @@ export const Loading = ({ emojis?: SymbolEmojiData[]; numEmojis?: number; }) => { + // Fetch/load the emoji picker data here to ensure that the picker has emoji data to use. + // Since the library only initializes when the picker component is rendered, the library won't have + // data on pages that don't use the picker component unless we explicitly call `init(...)` here. + useEffect(() => { + data.then((d) => { + init({ set: "native", data: d }); + }); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, []); const pathname = usePathname(); // Use the emojis in the path if we're on the `market` page. const emojisInPath = pathname @@ -54,16 +74,15 @@ export const Loading = ({ <>
-
- {emoji} -
+ emojis={emoji} + />
diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/chat/components/message-container/index.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/chat/components/message-container/index.tsx index 910d5637c..b71784487 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/chat/components/message-container/index.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/chat/components/message-container/index.tsx @@ -15,6 +15,7 @@ import { useAptos } from "context/wallet-context/AptosContextProvider"; import { formatDisplayName } from "@sdk/utils"; import { useNameStore } from "context/event-store-context"; import { motion } from "framer-motion"; +import { Emoji } from "utils/emoji"; const MessageContainer: React.FC = ({ index, @@ -53,12 +54,11 @@ const MessageContainer: React.FC = ({ - - {message.text} - + emojis={message.text} + /> @@ -72,9 +72,10 @@ const MessageContainer: React.FC = ({ {formatDisplayName(message.sender)} - - {message.senderRank} - + diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/emoji-not-found/index.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/emoji-not-found/index.tsx index 4f46a5a6e..03bb880fc 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/emoji-not-found/index.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/emoji-not-found/index.tsx @@ -1,6 +1,8 @@ import { Flex } from "@containers"; import { Text } from "components/text"; import { translationFunction } from "context/language-context"; +import { emoji } from "utils"; +import { Emoji } from "utils/emoji"; export const EmojiNotFound = () => { const { t } = translationFunction(); @@ -8,7 +10,7 @@ export const EmojiNotFound = () => { return ( - {t("Emoji not found.")} 😳 + {t("Emoji not found.")} ); diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/main-info/MainInfo.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/main-info/MainInfo.tsx index e35695420..f39064d48 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/main-info/MainInfo.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/main-info/MainInfo.tsx @@ -8,6 +8,7 @@ import { emojisToName } from "lib/utils/emojis-to-name-or-symbol"; import { useEventStore } from "context/event-store-context"; import { useLabelScrambler } from "components/pages/home/components/table-card/animation-variants/event-variants"; import { isMarketStateModel } from "@sdk/indexer-v2/types"; +import { Emoji } from "utils/emoji"; const innerWrapper = `flex flex-col md:flex-row justify-around w-full max-w-[1362px] px-[30px] lg:px-[44px] py-[17px] md:py-[37px] xl:py-[68px]`; @@ -55,9 +56,7 @@ const MainInfo = ({ data }: MainInfoProps) => { {emojisToName(data.emojis)}
-
- {data.symbolData.symbol} -
+
diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/CongratulationsToast.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/CongratulationsToast.tsx index 1dccb48a5..51cd669ce 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/CongratulationsToast.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/CongratulationsToast.tsx @@ -3,6 +3,8 @@ import { type AnyNumberString } from "@sdk/types/types"; import { ExplorerLink } from "components/link/component"; import { APTOS_NETWORK } from "lib/env"; import { toDisplayCoinDecimals } from "lib/utils/decimals"; +import { emoji } from "utils"; +import { Emoji } from "utils/emoji"; export const CongratulationsToast = ({ transactionHash, @@ -15,7 +17,7 @@ export const CongratulationsToast = ({ const amountString = toDisplayCoinDecimals({ num: amount, decimals: 2 }); return (
- 🎉 +
Congratulations! diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/InputLabels.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/InputLabels.tsx index ea388cadb..254f0cefa 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/InputLabels.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/InputLabels.tsx @@ -1,4 +1,5 @@ import AptosIconBlack from "@icons/AptosBlack"; +import { Emoji } from "utils/emoji"; export const AptosInputLabel = () => (
@@ -7,7 +8,8 @@ export const AptosInputLabel = () => ( ); export const EmojiInputLabel = ({ emoji }: { emoji: string }) => ( -
- {emoji} -
+ ); diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx index 4df0a192c..d3f6b3bd9 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx @@ -31,13 +31,15 @@ import { getTooltipStyles } from "components/selects/theme"; import { useThemeContext } from "context"; import { TradeOptions } from "components/selects/trade-options"; import { getMaxSlippageSettings } from "utils/slippage"; +import { Emoji } from "utils/emoji"; +import { type AnyEmojiName } from "@sdk/emoji_data/types"; -const SmallButton = ({ - emoji, +const SmallEmojiButton = ({ + emoji: emojiName, description, onClick, }: { - emoji: string; + emoji: AnyEmojiName; description: string; onClick?: MouseEventHandler; }) => { @@ -53,7 +55,7 @@ const SmallButton = ({ className="px-[.7rem] py-[.2rem] border-[1px] border-solid rounded-full border-dark-gray h-[1.5rem] cursor-pointer hover:bg-neutral-800" onClick={onClick} > -
{emoji}
+
); @@ -234,15 +236,15 @@ export default function SwapComponent({ {isSell ? ( <> - { setInputAmount(emojicoinBalance / 2n); }} /> - { setInputAmount(emojicoinBalance); @@ -251,22 +253,22 @@ export default function SwapComponent({ ) : ( <> - { setInputAmount(availableAptBalance / 4n); }} /> - { setInputAmount(availableAptBalance / 2n); }} /> - { setInputAmount(availableAptBalance); @@ -323,8 +325,8 @@ export default function SwapComponent({
-
- {emoji("gear")} +
+
{tooltip}
@@ -336,7 +338,7 @@ export default function SwapComponent({ })}{" "} APT {" "} - {emoji("fuel pump")} +
diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-history/index.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-history/index.tsx index 127dcc435..cf4701cc6 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-history/index.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-history/index.tsx @@ -10,6 +10,7 @@ import { motion } from "framer-motion"; import { type SwapEventModel } from "@sdk/indexer-v2/types"; import { type AccountAddressString } from "@sdk/emojicoin_dot_fun"; import "./trade-history.css"; +import { Emoji } from "utils/emoji"; const HARD_LIMIT = 500; @@ -87,7 +88,7 @@ const TradeHistory = (props: TradeHistoryProps) => { APT - {props.data.symbol} + Time diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-history/table-row/index.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-history/table-row/index.tsx index de189b2bf..80caa11e2 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-history/table-row/index.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-history/table-row/index.tsx @@ -10,6 +10,8 @@ import { formatDisplayName } from "@sdk/utils"; import Text from "components/text"; import Popup from "components/popup"; import { motion } from "framer-motion"; +import { emoji } from "utils"; +import { Emoji } from "utils/emoji"; type TableRowTextItemProps = { className: string; @@ -75,16 +77,16 @@ const TableRow = ({ - {item.rankIcon === "🐡" + {item.rankIcon === emoji("blowfish") ? "n00b" - : item.rankIcon === "🐳" + : item.rankIcon === emoji("spouting whale") ? "365 UR SO JULIA" : "SKILL ISSUE"} } >
- {item.rankIcon} +
diff --git a/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx b/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx index d803db9b3..35a51bc12 100644 --- a/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/emoji-table/index.tsx @@ -33,6 +33,7 @@ import { ROUTES } from "router/routes"; import { type HomePageProps } from "app/home/HomePage"; import { useReliableSubscribe } from "@hooks/use-reliable-subscribe"; import { SortMarketsBy } from "@sdk/indexer-v2/types/common"; +import { Emoji } from "utils/emoji"; export interface EmojiTableProps extends Omit {} @@ -178,7 +179,7 @@ const EmojiTable = (props: EmojiTableProps) => {
- Click here to launch {emojis.join("")} ! + Click here to launch {}
diff --git a/src/typescript/frontend/src/components/pages/home/components/main-card/MainCard.tsx b/src/typescript/frontend/src/components/pages/home/components/main-card/MainCard.tsx index 24d99555f..7f8adf43a 100644 --- a/src/typescript/frontend/src/components/pages/home/components/main-card/MainCard.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/main-card/MainCard.tsx @@ -13,7 +13,8 @@ import planetHome from "../../../../../../public/images/planet-home.png"; import { emojiNamesToPath } from "utils/pathname-helpers"; import { type HomePageProps } from "app/home/HomePage"; import "./module.css"; -import { SYMBOL_EMOJI_DATA } from "@sdk/emoji_data"; +import { emoji } from "utils"; +import { Emoji } from "utils/emoji"; export interface MainCardProps { featured?: HomePageProps["featured"]; @@ -83,26 +84,19 @@ const MainCard = (props: MainCardProps) => { ref={globeImage} placeholder="empty" /> - - {[...new Intl.Segmenter().segment(featured?.market.symbolData.symbol ?? "🖤")].length == - 1 ? ( -
- {featured?.market.symbolData.symbol ?? "🖤"} -
- ) : ( -
- {featured?.market.symbolData.symbol} -
- )} + -
- HOT +
+ HOT   - - {SYMBOL_EMOJI_DATA.byName("fire")?.emoji} - +
+ +
emojis.length <= 2 ? ("pixel-heading-1" as const) : ("pixel-heading-1b" as const); @@ -37,7 +38,6 @@ const getFontSize = (emojis: SymbolEmojiData[]) => const TableCard = ({ index, marketID, - symbol, emojis, staticMarketCap, staticVolume24H, @@ -224,9 +224,10 @@ const TableCard = ({ -
- {symbol} -
+ {emojis.map((emoji, i) => (
-
- {emoji.emoji} -
+ emojis={emoji.emoji} + />
- {color ? "🟩" : "⬜"} + {color ? emoji("green square") : emoji("white large square")} )), [emptyArray] diff --git a/src/typescript/frontend/src/components/pages/launch-emojicoin/memoized-launch/index.tsx b/src/typescript/frontend/src/components/pages/launch-emojicoin/memoized-launch/index.tsx index 6ebe222bc..4a1205da1 100644 --- a/src/typescript/frontend/src/components/pages/launch-emojicoin/memoized-launch/index.tsx +++ b/src/typescript/frontend/src/components/pages/launch-emojicoin/memoized-launch/index.tsx @@ -14,7 +14,9 @@ import { toCoinDecimalString } from "lib/utils/decimals"; import { MARKET_REGISTRATION_DEPOSIT, ONE_APT_BIGINT } from "@sdk/const"; import Info from "components/info"; import { filterBigEmojis } from "components/pages/emoji-picker/EmojiPicker"; +import { Emoji } from "utils/emoji"; import { useScramble } from "use-scramble"; +import { emoji } from "utils"; const labelClassName = "whitespace-nowrap body-sm md:body-lg text-light-gray uppercase font-forma"; @@ -118,14 +120,13 @@ export const MemoizedLaunchAnimation = ({
{t("Emojicoin Symbol:")}
-
- {emojis.join(", ")} -
+ emojis={emojis.join("")} + />
@@ -150,7 +151,10 @@ export const MemoizedLaunchAnimation = ({ {t("Your balance")}
{t("Your balance")} -
{sufficientBalance ? "✅" : "❌"}
+
diff --git a/src/typescript/frontend/src/components/pages/pools/components/pools-table/components/table-row-desktop/XprPopup.tsx b/src/typescript/frontend/src/components/pages/pools/components/pools-table/components/table-row-desktop/XprPopup.tsx index 9131800df..27db3e361 100644 --- a/src/typescript/frontend/src/components/pages/pools/components/pools-table/components/table-row-desktop/XprPopup.tsx +++ b/src/typescript/frontend/src/components/pages/pools/components/pools-table/components/table-row-desktop/XprPopup.tsx @@ -1,5 +1,7 @@ import { type ReactNode } from "react"; import { Big } from "big.js"; +import { emoji } from "utils"; +import { Emoji } from "utils/emoji"; const DAYS_IN_WEEK = 7; const DAYS_IN_YEAR = 365; @@ -13,7 +15,7 @@ export const getXPR = (x: number, tvlPerLpCoinGrowth: Big) => export const formatXPR = (time: number, bigDailyTvl: Big) => { if (bigDailyTvl.eq(Big(0))) { - return `⏳`; + return ; } const xprIn = getXPR(time, bigDailyTvl); const xpr = xprIn.toFixed(4); diff --git a/src/typescript/frontend/src/components/pages/pools/components/pools-table/components/table-row-desktop/index.tsx b/src/typescript/frontend/src/components/pages/pools/components/pools-table/components/table-row-desktop/index.tsx index a39b7fd79..c0276cde5 100644 --- a/src/typescript/frontend/src/components/pages/pools/components/pools-table/components/table-row-desktop/index.tsx +++ b/src/typescript/frontend/src/components/pages/pools/components/pools-table/components/table-row-desktop/index.tsx @@ -7,6 +7,7 @@ import { toCoinDecimalString } from "lib/utils/decimals"; import Popup from "components/popup"; import { Big } from "big.js"; import { formatXPR, XprPopup } from "./XprPopup"; +import { Emoji } from "utils/emoji"; const TableRowDesktop: React.FC = ({ item, selected, onClick }) => { const { isMobile } = useMatchBreakpoints(); @@ -23,7 +24,7 @@ const TableRowDesktop: React.FC = ({ item, selected, onCli ellipsis title={item.market.symbolData.symbol.toUpperCase()} > - {item.market.symbolData.symbol} + diff --git a/src/typescript/frontend/src/components/pages/pools/components/pools-table/constants/index.ts b/src/typescript/frontend/src/components/pages/pools/components/pools-table/constants/index.ts index a45bb9ec6..3b27d9ef2 100644 --- a/src/typescript/frontend/src/components/pages/pools/components/pools-table/constants/index.ts +++ b/src/typescript/frontend/src/components/pages/pools/components/pools-table/constants/index.ts @@ -46,48 +46,3 @@ export const MOBILE_HEADERS = [ sortBy: "apr", }, ]; - -export const DATA = [ - { - pool: "🖤", - allTime: 1000000, - vol24: 1000000, - tvl: 7777777, - apr: 11, - }, - { - pool: "🖤", - allTime: 2000000, - vol24: 2000000, - tvl: 8777777, - apr: 21, - }, - { - pool: "🖤", - allTime: 3000000, - vol24: 3000000, - tvl: 9777777, - apr: 31, - }, - { - pool: "🖤", - allTime: 1000000, - vol24: 1000000, - tvl: 7777777, - apr: 11, - }, - { - pool: "🖤", - allTime: 2000000, - vol24: 2000000, - tvl: 8777777, - apr: 21, - }, - { - pool: "🖤", - allTime: 3000000, - vol24: 3000000, - tvl: 9777777, - apr: 31, - }, -]; diff --git a/src/typescript/frontend/src/components/pages/verify_status/VerifyStatusPage.tsx b/src/typescript/frontend/src/components/pages/verify_status/VerifyStatusPage.tsx index fe2ed0849..3468fab70 100644 --- a/src/typescript/frontend/src/components/pages/verify_status/VerifyStatusPage.tsx +++ b/src/typescript/frontend/src/components/pages/verify_status/VerifyStatusPage.tsx @@ -8,10 +8,12 @@ import { motion } from "framer-motion"; import { standardizeAddress, truncateAddress } from "@sdk/utils"; import { getVerificationStatus } from "./get-verification-status"; import { EXTERNAL_LINK_PROPS } from "components/link"; +import { emoji } from "utils"; +import { Emoji } from "utils/emoji"; -const checkmarkOrX = (bool: boolean) => { - return {bool ? "✅" : "❌"} ; -}; +const checkmarkOrX = (bool: boolean) => ( + +); export const ClientVerifyPage: React.FC<{ geoblocked: boolean }> = ({ geoblocked }) => { const { account } = useAptos(); diff --git a/src/typescript/frontend/src/components/price-feed/inner.tsx b/src/typescript/frontend/src/components/price-feed/inner.tsx index 9332cca21..932bc5741 100644 --- a/src/typescript/frontend/src/components/price-feed/inner.tsx +++ b/src/typescript/frontend/src/components/price-feed/inner.tsx @@ -3,6 +3,7 @@ import type { fetchPriceFeed } from "@/queries/home"; import Link from "next/link"; import Carousel from "components/carousel"; +import { Emoji } from "utils/emoji"; const Item = ({ emoji, change }: { emoji: string; change: number }) => { return ( @@ -11,7 +12,7 @@ const Item = ({ emoji, change }: { emoji: string; change: number }) => { className={`font-pixelar whitespace-nowrap border-[1px] border-solid ${change >= 0 ? "border-green" : "border-pink"} rounded-full px-3 py-[2px] select-none mr-[22px]`} draggable={false} > - {emoji}: + = 0 ? "text-green" : "text-pink"}`}> {change >= 0 ? "+" : "-"} {Math.abs(change).toFixed(2)}% diff --git a/src/typescript/frontend/src/components/wallet/WalletDropdownMenu.tsx b/src/typescript/frontend/src/components/wallet/WalletDropdownMenu.tsx index 45403289c..236c6d6fc 100644 --- a/src/typescript/frontend/src/components/wallet/WalletDropdownMenu.tsx +++ b/src/typescript/frontend/src/components/wallet/WalletDropdownMenu.tsx @@ -18,6 +18,8 @@ import { EXTERNAL_LINK_PROPS } from "components/link"; import { WalletDropdownItem } from "./WalletDropdownItem"; import { useAptos } from "context/wallet-context/AptosContextProvider"; import { useNameStore } from "context/event-store-context"; +import { emoji } from "utils"; +import { Emoji } from "utils/emoji"; const WIDTH = "24ch"; @@ -66,13 +68,19 @@ const WalletDropdownMenu = () => { diff --git a/src/typescript/frontend/src/context/ConnectToWebSockets.tsx b/src/typescript/frontend/src/context/ConnectToWebSockets.tsx index 6190368f7..c4ceff14b 100644 --- a/src/typescript/frontend/src/context/ConnectToWebSockets.tsx +++ b/src/typescript/frontend/src/context/ConnectToWebSockets.tsx @@ -1,5 +1,7 @@ import { useEventStore } from "./event-store-context/hooks"; import { motion } from "framer-motion"; +import { emoji } from "utils"; +import { Emoji } from "utils/emoji"; import { hexToRgba } from "utils/hex-to-rgba"; export const ConnectToWebSockets = () => { @@ -11,7 +13,10 @@ export const ConnectToWebSockets = () => { {process.env.NODE_ENV === "development" && (
-
{connected ? "🟢" : "⚫"}
+ ; export type SubmissionResponse = Promise<{ @@ -149,12 +150,12 @@ export function AptosContextProvider({ if (!account?.address) return; try { await navigator.clipboard.writeText(account.address); - toast.success("Copied address to clipboard! 📋", { + toast.success(`Copied address to clipboard! ${emoji("clipboard")}`, { pauseOnFocusLoss: false, autoClose: 2000, }); } catch { - toast.error("Failed to copy address to clipboard. 😓", { + toast.error(`Failed to copy address to clipboard. ${emoji("downcast face with sweat")}`, { pauseOnFocusLoss: false, autoClose: 2000, }); diff --git a/src/typescript/frontend/src/context/wallet-context/WalletItem.tsx b/src/typescript/frontend/src/context/wallet-context/WalletItem.tsx index 0c6456c84..9798e8618 100644 --- a/src/typescript/frontend/src/context/wallet-context/WalletItem.tsx +++ b/src/typescript/frontend/src/context/wallet-context/WalletItem.tsx @@ -13,6 +13,8 @@ import RiseIcon from "@icons/RiseIcon"; import NightlyIcon from "@icons/NightlyIcon"; import { Arrow } from "components/svg"; import { useScramble } from "use-scramble"; +import { Emoji } from "utils/emoji"; +import { emoji } from "utils"; const IconProps = { width: 28, @@ -117,12 +119,14 @@ export const WalletItem: React.FC<{
{wallet.name === current?.name ? ( <> -

- {"⚡"} -

-

- {"❌"} -

+ + ) : ( diff --git a/src/typescript/frontend/src/lib/store/emoji-picker-store.ts b/src/typescript/frontend/src/lib/store/emoji-picker-store.ts index c973981c8..68a242001 100644 --- a/src/typescript/frontend/src/lib/store/emoji-picker-store.ts +++ b/src/typescript/frontend/src/lib/store/emoji-picker-store.ts @@ -1,10 +1,10 @@ -import { type SymbolEmojiData } from "@sdk/emoji_data"; +import { type AnyEmoji, type SymbolEmojiData } from "@sdk/emoji_data"; import { createStore } from "zustand"; import { insertEmojiTextInputHelper, removeEmojiTextInputHelper } from "./emoji-picker-utils"; export type EmojiPickerState = { mode: "chat" | "register" | "search"; - emojis: string[]; + emojis: AnyEmoji[]; nativePicker: boolean; pickerRef: HTMLDivElement | null; textAreaRef: HTMLTextAreaElement | null; @@ -25,7 +25,7 @@ export type EmojiPickerState = { export type EmojiPickerActions = { clear: () => void; - setEmojis: (emojis: string[]) => void; + setEmojis: (emojis: AnyEmoji[]) => void; setNativePicker: (value: boolean) => void; setPickerRef: (value: HTMLDivElement | null) => void; setTextAreaRef: (value: HTMLTextAreaElement | null) => void; @@ -36,7 +36,7 @@ export type EmojiPickerActions = { setIsLoadingRegisteredMarket: (value: boolean) => void; insertEmojiTextInput: (textToInsert: string | string[]) => void; removeEmojiTextInput: (key?: string) => void; - getEmojis: () => string[]; + getEmojis: () => AnyEmoji[]; }; export type EmojiPickerStore = EmojiPickerState & EmojiPickerActions; @@ -61,7 +61,7 @@ export const setInputHelper = ({ shouldFocus, }: { textAreaRef: HTMLTextAreaElement | null; - emojis: string[]; + emojis: AnyEmoji[]; selectionStart?: number; selectionEnd?: number; shouldFocus?: boolean; diff --git a/src/typescript/frontend/src/lib/store/emoji-picker-utils.ts b/src/typescript/frontend/src/lib/store/emoji-picker-utils.ts index 8e28bb6d4..ddfeac9ad 100644 --- a/src/typescript/frontend/src/lib/store/emoji-picker-utils.ts +++ b/src/typescript/frontend/src/lib/store/emoji-picker-utils.ts @@ -48,7 +48,7 @@ export const insertEmojiTextInputHelper = ( ); // prettier-ignore - const newEmojis = (emojis as Array) + const newEmojis = (emojis) .slice(0, start) .concat(filteredEmojis) .concat(emojis.slice(end)); diff --git a/src/typescript/frontend/src/lib/store/server-to-client/StoreOnClient.tsx b/src/typescript/frontend/src/lib/store/server-to-client/StoreOnClient.tsx index fffdfb760..5d7023622 100644 --- a/src/typescript/frontend/src/lib/store/server-to-client/StoreOnClient.tsx +++ b/src/typescript/frontend/src/lib/store/server-to-client/StoreOnClient.tsx @@ -8,6 +8,7 @@ import { useEventStore } from "context/event-store-context"; import { useAptos } from "context/wallet-context/AptosContextProvider"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; +import { emoji } from "utils"; export const StoreOnClient = () => { const { account, connect, connected, wallet } = useWallet(); @@ -81,7 +82,7 @@ export const StoreOnClient = () => { } }} > - {"Fund me 💰"} + {"Fund me"} {emoji("money bag")}
diff --git a/src/typescript/frontend/src/utils/emoji.tsx b/src/typescript/frontend/src/utils/emoji.tsx new file mode 100644 index 000000000..29b0ae280 --- /dev/null +++ b/src/typescript/frontend/src/utils/emoji.tsx @@ -0,0 +1,36 @@ +import { type AnyEmojiData, getEmojisInString } from "@sdk/index"; +import { type DetailedHTMLProps, type HTMLAttributes } from "react"; + +import * as React from "react"; +declare global { + /* eslint-disable-next-line @typescript-eslint/no-namespace */ + namespace JSX { + interface IntrinsicElements { + "em-emoji": React.DetailedHTMLProps, HTMLElement> & { + size?: string; + native?: string; + key?: string; + }; + } + } +} + +export const Emoji = ({ + emojis, + ...props +}: Omit, HTMLSpanElement>, "children"> & { + emojis: AnyEmojiData[] | string; +}) => { + let data: React.ReactNode[] = []; + if (typeof emojis === "string") { + const emojisInString = getEmojisInString(emojis); + data = emojisInString.map((e, i) => ( + + )); + } else { + data = emojis.map((e, i) => ( + + )); + } + return {data}; +}; diff --git a/src/typescript/frontend/src/utils/index.ts b/src/typescript/frontend/src/utils/index.ts index f1947cdf7..b91da81a1 100644 --- a/src/typescript/frontend/src/utils/index.ts +++ b/src/typescript/frontend/src/utils/index.ts @@ -1,4 +1,4 @@ -import { SYMBOL_EMOJI_DATA } from "@sdk/emoji_data"; +import { type AnyEmojiName, CHAT_EMOJI_DATA, SYMBOL_EMOJI_DATA } from "@sdk/emoji_data"; export { checkIsEllipsis } from "./check-is-ellipsis"; export { getFileNameFromSrc } from "./get-file-name-from-src"; @@ -23,4 +23,7 @@ export const parseJSON = (json: string): T => return value as T; }); -export const emoji = (name: string): string => SYMBOL_EMOJI_DATA.byName(name)!.emoji; +export const emoji = (name: AnyEmojiName) => + SYMBOL_EMOJI_DATA.hasName(name) + ? SYMBOL_EMOJI_DATA.byStrictName(name).emoji + : CHAT_EMOJI_DATA.byStrictName(name).emoji; diff --git a/src/typescript/package.json b/src/typescript/package.json index 103b52ce5..4e734dfab 100644 --- a/src/typescript/package.json +++ b/src/typescript/package.json @@ -29,8 +29,8 @@ "docker:prune": "../docker/utils/prune.sh --reset-localnet --yes", "docker:restart": "pnpm run docker:prune && pnpm run docker:up", "docker:up": "docker compose -f ../docker/compose.local.yaml --env-file ../docker/example.local.env up -d", - "format": "turbo run format -- --write", - "format:check": "turbo run format -- --check", + "format": "turbo run format", + "format:check": "turbo run format:check", "lint": "turbo run lint", "lint:fix": "turbo run lint -- --fix", "load-env": "dotenv -e .env", diff --git a/src/typescript/sdk/package.json b/src/typescript/sdk/package.json index 6c5eadadf..70478deb5 100644 --- a/src/typescript/sdk/package.json +++ b/src/typescript/sdk/package.json @@ -53,7 +53,7 @@ "name": "@econia-labs/emojicoin-sdk", "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a", "scripts": { - "_format": "prettier 'src/**/*.ts' 'tests/**/*.ts' '.eslintrc.js'", + "_format": "prettier -l 'src/**/*.ts' 'tests/**/*.ts' '.eslintrc.js'", "build": "tsc", "build:debug": "BUILD_DEBUG=true pnpm run build", "build:no-checks": "tsc --skipLibCheck", diff --git a/src/typescript/sdk/src/emoji_data/types.ts b/src/typescript/sdk/src/emoji_data/types.ts index fb17e6839..8fdd84afa 100644 --- a/src/typescript/sdk/src/emoji_data/types.ts +++ b/src/typescript/sdk/src/emoji_data/types.ts @@ -15,6 +15,9 @@ export type AllChatEmojiData = typeof AllChatEmojiJSON; export type ChatEmoji = keyof AllChatEmojiData; export type ChatEmojiName = keyof typeof ChatNamesJSON; +export type AnyEmoji = SymbolEmoji | ChatEmoji; +export type AnyEmojiName = SymbolEmojiName | ChatEmojiName; + /** * A single symbol emoji's data. */ @@ -40,6 +43,8 @@ export type ChatEmojiData = { emoji: ChatEmoji; }; +export type AnyEmojiData = SymbolEmojiData | ChatEmojiData; + /** * The final concatenated symbol data for a market symbol. This will consist of data for one or * more symbol emojis. diff --git a/src/typescript/sdk/src/emoji_data/utils.ts b/src/typescript/sdk/src/emoji_data/utils.ts index 1a14b2300..70b15ed70 100644 --- a/src/typescript/sdk/src/emoji_data/utils.ts +++ b/src/typescript/sdk/src/emoji_data/utils.ts @@ -7,13 +7,14 @@ import { type SymbolEmojiName, type SymbolData, type SymbolEmoji, + type AnyEmoji, } from "./types"; import { MAX_SYMBOL_LENGTH } from "../const"; -export const getEmojisInString = (symbols: string): Array => { +export const getEmojisInString = (symbols: string): Array => { const regex = emojiRegex(); const matches = symbols.matchAll(regex); - return Array.from(matches).map((match) => match[0]) as Array; + return Array.from(matches).map((match) => match[0]) as Array; }; export const getSymbolEmojisInString = (symbols: string): SymbolEmoji[] => { From 0592eb8568f11f251a6a4db32d3adeeb943c8e83 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Wed, 13 Nov 2024 06:32:23 +0100 Subject: [PATCH 31/94] [ECO-2380] Add PostgREST logging env vars to AWS config (#345) Co-authored-by: alnoki <43892045+alnoki@users.noreply.github.com> --- src/cloud-formation/indexer.cfn.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/cloud-formation/indexer.cfn.yaml b/src/cloud-formation/indexer.cfn.yaml index 495bbc6f0..ff1934bd5 100644 --- a/src/cloud-formation/indexer.cfn.yaml +++ b/src/cloud-formation/indexer.cfn.yaml @@ -210,9 +210,21 @@ Parameters: PostgrestImageVersion: Default: 'v12.2.3' Type: 'String' + PostgrestLogLevel: + Default: 'info' + Type: 'String' PostgrestMaxRows: Default: 500 Type: 'Number' + PostgrestPlanEnabled: + Default: 'true' + Type: 'String' + PostgrestServerTimingEnabled: + Default: 'true' + Type: 'String' + PostgrestServerTraceHeader: + Default: 'X-Request-Id' + Type: 'String' ProcessorImageVersion: Type: 'String' VpcStackName: @@ -1233,6 +1245,14 @@ Resources: Value: 'web_anon' - Name: 'PGRST_DB_MAX_ROWS' Value: !Ref 'PostgrestMaxRows' + - Name: 'PGRST_LOG_LEVEL' + Value: !Ref 'PostgrestLogLevel' + - Name: 'PGRST_DB_PLAN_ENABLED' + Value: !Ref 'PostgrestPlanEnabled' + - Name: 'PGRST_SERVER_TIMING_ENABLED' + Value: !Ref 'PostgrestServerTimingEnabled' + - Name: 'PGRST_SERVER_TRACE_HEADER' + Value: !Ref 'PostgrestServerTraceHeader' Image: !Join - '' - - !Ref 'AWS::AccountId' From f78eed5136481340f2762d732755a92d6d4c8a67 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Wed, 13 Nov 2024 19:30:33 +0100 Subject: [PATCH 32/94] [ECO-2395] Enable Performance Insights for DB (#347) --- src/cloud-formation/indexer.cfn.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cloud-formation/indexer.cfn.yaml b/src/cloud-formation/indexer.cfn.yaml index ff1934bd5..a35655785 100644 --- a/src/cloud-formation/indexer.cfn.yaml +++ b/src/cloud-formation/indexer.cfn.yaml @@ -839,7 +839,9 @@ Resources: Properties: DBClusterIdentifier: !Ref 'DbCluster' DBInstanceClass: 'db.serverless' + EnablePerformanceInsights: true Engine: 'aurora-postgresql' + PerformanceInsightsRetentionPeriod: 31 Type: 'AWS::RDS::DBInstance' # Replica (reader) database instance. DbInstanceReplica: @@ -847,7 +849,9 @@ Resources: Properties: DBClusterIdentifier: !Ref 'DbCluster' DBInstanceClass: 'db.serverless' + EnablePerformanceInsights: true Engine: 'aurora-postgresql' + PerformanceInsightsRetentionPeriod: 31 Type: 'AWS::RDS::DBInstance' # Security group for the database itself. DbSecurityGroup: From 006a4fdaf36d190e87c7536d4213531f0eb59ebd Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Wed, 13 Nov 2024 20:45:02 +0100 Subject: [PATCH 33/94] [ECO-2373] Add deposit/withdraw pills to liquidity page (#342) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- .../frontend/src/components/EmojiPill.tsx | 36 ++++++++ .../trade-emojicoin/SwapComponent.tsx | 49 ++-------- .../home/components/main-card/MainCard.tsx | 2 +- .../pools/components/liquidity/index.tsx | 90 ++++++++++++++----- 4 files changed, 114 insertions(+), 63 deletions(-) create mode 100644 src/typescript/frontend/src/components/EmojiPill.tsx diff --git a/src/typescript/frontend/src/components/EmojiPill.tsx b/src/typescript/frontend/src/components/EmojiPill.tsx new file mode 100644 index 000000000..dbc8b08dd --- /dev/null +++ b/src/typescript/frontend/src/components/EmojiPill.tsx @@ -0,0 +1,36 @@ +import { type MouseEventHandler } from "react"; +import { Text } from "components/text"; +import { Emoji } from "utils/emoji"; +import Popup from "./popup"; +import { emoji } from "utils"; +import { type AnyEmojiName } from "@sdk/emoji_data/types"; + +export const EmojiPill = ({ + emoji: emojiName, + description, + onClick, +}: { + emoji: AnyEmojiName; + description: string; + onClick?: MouseEventHandler; +}) => { + return ( + + {description} + + } + > +
+ +
+
+ ); +}; diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx index d3f6b3bd9..bfd3de81a 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx @@ -1,13 +1,7 @@ "use client"; import { AptosInputLabel, EmojiInputLabel } from "./InputLabels"; -import { - type PropsWithChildren, - useEffect, - useState, - useMemo, - type MouseEventHandler, -} from "react"; +import { type PropsWithChildren, useEffect, useState, useMemo } from "react"; import FlipInputsArrow from "./FlipInputsArrow"; import { Column, Row } from "components/layout/components/FlexContainers"; import { SwapButton } from "./SwapButton"; @@ -23,8 +17,6 @@ import { useAptos } from "context/wallet-context/AptosContextProvider"; import { AnimatePresence, motion } from "framer-motion"; import { toCoinTypes } from "@sdk/markets/utils"; import { Flex, FlexGap } from "@containers"; -import Popup from "components/popup"; -import { Text } from "components/text"; import { InputNumeric } from "components/inputs"; import { emoji } from "utils"; import { getTooltipStyles } from "components/selects/theme"; @@ -32,34 +24,7 @@ import { useThemeContext } from "context"; import { TradeOptions } from "components/selects/trade-options"; import { getMaxSlippageSettings } from "utils/slippage"; import { Emoji } from "utils/emoji"; -import { type AnyEmojiName } from "@sdk/emoji_data/types"; - -const SmallEmojiButton = ({ - emoji: emojiName, - description, - onClick, -}: { - emoji: AnyEmojiName; - description: string; - onClick?: MouseEventHandler; -}) => { - return ( - - {description} - - } - > -
- -
-
- ); -}; +import { EmojiPill } from "components/EmojiPill"; const SimulateInputsWrapper = ({ children }: PropsWithChildren) => (
{children}
@@ -236,14 +201,14 @@ export default function SwapComponent({ {isSell ? ( <> - { setInputAmount(emojicoinBalance / 2n); }} /> - { @@ -253,21 +218,21 @@ export default function SwapComponent({ ) : ( <> - { setInputAmount(availableAptBalance / 4n); }} /> - { setInputAmount(availableAptBalance / 2n); }} /> - { diff --git a/src/typescript/frontend/src/components/pages/home/components/main-card/MainCard.tsx b/src/typescript/frontend/src/components/pages/home/components/main-card/MainCard.tsx index 7f8adf43a..53b436073 100644 --- a/src/typescript/frontend/src/components/pages/home/components/main-card/MainCard.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/main-card/MainCard.tsx @@ -91,7 +91,7 @@ const MainCard = (props: MainCardProps) => { -
+
HOT  
diff --git a/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx b/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx index 24f7d4650..e44a099dc 100644 --- a/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx +++ b/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx @@ -28,6 +28,7 @@ import { TypeTag } from "@aptos-labs/ts-sdk"; import Info from "components/info"; import { type AnyNumberString } from "@sdk/types/types"; import { type PoolsData } from "../../ClientPoolsPage"; +import { EmojiPill } from "components/EmojiPill"; type LiquidityProps = { market: PoolsData | undefined; @@ -253,29 +254,78 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { return ( - - - - {t(direction === "add" ? "Add liquidity" : "Remove liquidity")} - - - - + + + + + + {t(direction === "add" ? "Add liquidity" : "Remove liquidity")} - - - + + + Liquidity providers receive a 0.25% fee from all trades, proportional to their + pool share. Fees are continuously reinvested in the pool and can be claimed by + withdrawing liquidity. + + + + + + {direction === "add" ? ( + <> + { + setLiquidity(aptBalance / 4n); + }} + /> + { + setLiquidity(aptBalance / 2n); + }} + /> + { + setLiquidity(aptBalance); + }} + /> + + ) : ( + <> + { + setLP(emojicoinLPBalance / 2n); + }} + /> + { + setLP(emojicoinLPBalance); + }} + /> + + )} + {direction === "add" ? ( From c6bb54ae788285ee76cb11cc55aca213a3f71da1 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:44:39 -0800 Subject: [PATCH 34/94] [ECO-2389] Render `/launch` as a static page (#335) Co-authored-by: Bogdan Crisan --- .../frontend/src/app/home/HomePage.tsx | 3 -- src/typescript/frontend/src/app/home/page.tsx | 5 -- .../frontend/src/app/launch/page.tsx | 7 ++- src/typescript/frontend/src/app/layout.tsx | 5 +- .../frontend/src/app/market/[market]/page.tsx | 5 -- .../frontend/src/app/pools/page.tsx | 6 +-- .../frontend/src/app/verify/page.tsx | 8 ++-- .../frontend/src/app/verify_status/page.tsx | 7 +-- .../emoji-picker/EmojiPickerWithInput.tsx | 24 ++-------- .../src/components/geoblocking/index.tsx | 48 +++++++++++-------- .../header/components/mobile-menu/index.tsx | 5 -- .../header/components/mobile-menu/types.ts | 1 - .../frontend/src/components/header/index.tsx | 13 ++--- .../frontend/src/components/header/types.ts | 1 - .../wallet-button/ConnectWalletButton.tsx | 12 ++++- .../src/components/inputs/search-bar.tsx | 3 +- .../pages/emojicoin/ClientEmojicoinPage.tsx | 6 +-- .../emojicoin/components/chat/ChatBox.tsx | 3 +- .../components/desktop-grid/index.tsx | 5 +- .../components/mobile-grid/index.tsx | 5 +- .../trade-emojicoin/LiquidityButton.tsx | 2 +- .../components/trade-emojicoin/SwapButton.tsx | 4 +- .../trade-emojicoin/SwapComponent.tsx | 2 - .../src/components/pages/emojicoin/types.ts | 4 -- .../home/components/emoji-table/index.tsx | 2 +- .../ClientLaunchEmojicoinPage.tsx | 4 +- .../components/launch-or-goto.tsx | 4 +- .../memoized-launch/index.tsx | 11 +---- .../pages/pools/ClientPoolsPage.tsx | 14 ++---- .../pools/components/liquidity/index.tsx | 5 +- .../components/pages/verify/VerifyPage.tsx | 4 +- .../pages/verify_status/VerifyStatusPage.tsx | 15 ++++-- .../frontend/src/context/providers.tsx | 20 ++------ .../wallet-context/AptosContextProvider.tsx | 7 ++- .../src/hooks/use-is-user-geoblocked.ts | 23 +++++++++ .../frontend/src/utils/geolocation.ts | 6 ++- 36 files changed, 124 insertions(+), 175 deletions(-) create mode 100644 src/typescript/frontend/src/hooks/use-is-user-geoblocked.ts diff --git a/src/typescript/frontend/src/app/home/HomePage.tsx b/src/typescript/frontend/src/app/home/HomePage.tsx index 793752202..85dc75754 100644 --- a/src/typescript/frontend/src/app/home/HomePage.tsx +++ b/src/typescript/frontend/src/app/home/HomePage.tsx @@ -13,7 +13,6 @@ export interface HomePageProps { sortBy: MarketDataSortByHomePage; searchBytes?: string; children?: React.ReactNode; - geoblocked: boolean; priceFeed: Array; } @@ -25,7 +24,6 @@ export default async function HomePageComponent({ sortBy, searchBytes, children, - geoblocked, priceFeed, }: HomePageProps) { return ( @@ -46,7 +44,6 @@ export default async function HomePageComponent({ page={page} sortBy={sortBy} searchBytes={searchBytes} - geoblocked={geoblocked} />
diff --git a/src/typescript/frontend/src/app/home/page.tsx b/src/typescript/frontend/src/app/home/page.tsx index fb7d43dab..47647c595 100644 --- a/src/typescript/frontend/src/app/home/page.tsx +++ b/src/typescript/frontend/src/app/home/page.tsx @@ -1,7 +1,5 @@ import { type HomePageParams, toHomePageParamsWithDefault } from "lib/routes/home-page-params"; import HomePageComponent from "./HomePage"; -import { isUserGeoblocked } from "utils/geolocation"; -import { headers } from "next/headers"; import { fetchFeaturedMarket, fetchMarkets, @@ -46,8 +44,6 @@ export default async function Home({ searchParams }: HomePageParams) { const priceFeed = await fetchPriceFeed({}); - // Call this last because `headers()` is a dynamic API and all fetches after this aren't cached. - const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); return ( ); diff --git a/src/typescript/frontend/src/app/launch/page.tsx b/src/typescript/frontend/src/app/launch/page.tsx index fac208f2c..49b74c15f 100644 --- a/src/typescript/frontend/src/app/launch/page.tsx +++ b/src/typescript/frontend/src/app/launch/page.tsx @@ -1,15 +1,14 @@ import ClientLaunchEmojicoinPage from "../../components/pages/launch-emojicoin/ClientLaunchEmojicoinPage"; -import { isUserGeoblocked } from "utils/geolocation"; -import { headers } from "next/headers"; import { type Metadata } from "next"; import { emoji } from "utils"; +export const dynamic = "force-static"; + export const metadata: Metadata = { title: "launch", description: `Launch your own emojicoins using emojicoin.fun ${emoji("party popper")}`, }; export default async function LaunchEmojicoinPage() { - const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); - return ; + return ; } diff --git a/src/typescript/frontend/src/app/layout.tsx b/src/typescript/frontend/src/app/layout.tsx index 9f3e3c768..2a7f8d937 100644 --- a/src/typescript/frontend/src/app/layout.tsx +++ b/src/typescript/frontend/src/app/layout.tsx @@ -11,8 +11,6 @@ import { } from "styles/fonts"; import "../app/global.css"; import DisplayDebugData from "@/store/server-to-client/FetchFromServer"; -import { isUserGeoblocked } from "utils/geolocation"; -import { headers } from "next/headers"; export const metadata: Metadata = getDefaultMetadata(); export const viewport: Viewport = { @@ -23,12 +21,11 @@ const fonts = [pixelar, formaDJRMicro, formaDJRDisplayMedium, formaDJRDisplayReg const fontsClassName = fonts.map((font) => font.variable).join(" "); export default async function RootLayout({ children }: { children: React.ReactNode }) { - const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); return ( - + {children} diff --git a/src/typescript/frontend/src/app/market/[market]/page.tsx b/src/typescript/frontend/src/app/market/[market]/page.tsx index d998e2e8b..6ca6b7f78 100644 --- a/src/typescript/frontend/src/app/market/[market]/page.tsx +++ b/src/typescript/frontend/src/app/market/[market]/page.tsx @@ -3,8 +3,6 @@ import EmojiNotFoundPage from "./not-found"; import { fetchContractMarketView } from "lib/queries/aptos-client/market-view"; import { SYMBOL_EMOJI_DATA } from "@sdk/emoji_data"; import { pathToEmojiNames } from "utils/pathname-helpers"; -import { isUserGeoblocked } from "utils/geolocation"; -import { headers } from "next/headers"; import { fetchChatEvents, fetchMarketState, fetchSwapEvents } from "@/queries/market"; import { deriveEmojicoinPublisherAddress } from "@sdk/emojicoin_dot_fun"; import { type Metadata } from "next"; @@ -77,8 +75,6 @@ const EmojicoinPage = async (params: EmojicoinPageProps) => { const swaps = await fetchSwapEvents({ marketID }); const marketView = await fetchContractMarketView(marketAddress.toString()); - // Call this last because `headers()` is a dynamic API and all fetches after this aren't cached. - const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); return ( { marketView, ...state.market, }} - geoblocked={geoblocked} /> ); } diff --git a/src/typescript/frontend/src/app/pools/page.tsx b/src/typescript/frontend/src/app/pools/page.tsx index 566dc2dc8..d6243317c 100644 --- a/src/typescript/frontend/src/app/pools/page.tsx +++ b/src/typescript/frontend/src/app/pools/page.tsx @@ -1,6 +1,4 @@ import ClientPoolsPage, { type PoolsData } from "components/pages/pools/ClientPoolsPage"; -import { headers } from "next/headers"; -import { isUserGeoblocked } from "utils/geolocation"; import { getPoolData } from "./api/getPoolDataQuery"; import { SortMarketsBy } from "@sdk/indexer-v2/types/common"; import { symbolBytesToEmojis } from "@sdk/emoji_data/utils"; @@ -26,7 +24,5 @@ export default async function PoolsPage({ searchParams }: { searchParams: { pool ) ); - // Call this last because `headers()` is a dynamic API and all fetches after this aren't cached. - const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); - return ; + return ; } diff --git a/src/typescript/frontend/src/app/verify/page.tsx b/src/typescript/frontend/src/app/verify/page.tsx index 71d2f67cb..c9406faaa 100644 --- a/src/typescript/frontend/src/app/verify/page.tsx +++ b/src/typescript/frontend/src/app/verify/page.tsx @@ -4,21 +4,19 @@ import { COOKIE_FOR_HASHED_ADDRESS, } from "components/pages/verify/session-info"; import { authenticate } from "components/pages/verify/verify"; -import { cookies, headers } from "next/headers"; +import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { ROUTES } from "router/routes"; -import { isUserGeoblocked } from "utils/geolocation"; export const dynamic = "force-dynamic"; const Verify = async () => { const hashed = cookies().get(COOKIE_FOR_HASHED_ADDRESS)?.value; const address = cookies().get(COOKIE_FOR_ACCOUNT_ADDRESS)?.value; - const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); let authenticated = false; if (!hashed || !address) { - return ; + return ; } else { authenticated = await authenticate({ address, @@ -30,7 +28,7 @@ const Verify = async () => { } } - return ; + return ; }; export default Verify; diff --git a/src/typescript/frontend/src/app/verify_status/page.tsx b/src/typescript/frontend/src/app/verify_status/page.tsx index 71dabc57e..7ba7bf154 100644 --- a/src/typescript/frontend/src/app/verify_status/page.tsx +++ b/src/typescript/frontend/src/app/verify_status/page.tsx @@ -1,12 +1,7 @@ import VerifyStatusPage from "components/pages/verify_status/VerifyStatusPage"; -import { headers } from "next/headers"; -import { isUserGeoblocked } from "utils/geolocation"; - -export const dynamic = "force-dynamic"; const Verify = async () => { - const geoblocked = await isUserGeoblocked(headers().get("x-real-ip")); - return ; + return ; }; export default Verify; diff --git a/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx b/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx index 66a1836ed..498e1803a 100644 --- a/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx +++ b/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx @@ -13,27 +13,21 @@ import { MarketValidityIndicator } from "./ColoredBytesIndicator"; import { variants } from "./animation-variants"; import { checkTargetAndStopDefaultPropagation } from "./utils"; import { getEmojisInString } from "@sdk/emoji_data"; -import "./triangle.css"; import { createPortal } from "react-dom"; import { type EmojiMartData } from "components/pages/emoji-picker/types"; import { useWallet } from "@aptos-labs/wallet-adapter-react"; +import "./triangle.css"; const EMOJI_FONT_FAMILY = '"EmojiMart", "Segoe UI Emoji", "Segoe UI Symbol", ' + '"Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", ' + '"Android Emoji"'; -const ChatInputBox = ({ - children, - geoblocked, -}: { - children: React.ReactNode; - geoblocked: boolean; -}) => { +const ChatInputBox = ({ children }: { children: React.ReactNode }) => { const { connected } = useWallet(); return ( <> - + {children} {!connected && ( @@ -50,17 +44,11 @@ const ChatInputBox = ({ const ConditionalWrapper = ({ children, mode, - geoblocked, }: { children: React.ReactNode; mode: "chat" | "register" | "search"; - geoblocked: boolean; }) => { - return mode === "chat" ? ( - {children} - ) : ( - <>{children} - ); + return mode === "chat" ? {children} : <>{children}; }; export const EmojiPickerWithInput = ({ @@ -68,14 +56,12 @@ export const EmojiPickerWithInput = ({ pickerButtonClassName, inputGroupProps, inputClassName = "", - geoblocked, filterEmojis, }: { handleClick: (message: string) => Promise; pickerButtonClassName?: string; inputGroupProps?: Partial>; inputClassName?: string; - geoblocked: boolean; filterEmojis?: (e: EmojiMartData["emojis"][string]) => boolean; }) => { const inputRef = useRef(null); @@ -216,7 +202,7 @@ export const EmojiPickerWithInput = ({ className="justify-center" ref={inputRef} > - +
diff --git a/src/typescript/frontend/src/components/geoblocking/index.tsx b/src/typescript/frontend/src/components/geoblocking/index.tsx index 46b5957f5..a56c47c1d 100644 --- a/src/typescript/frontend/src/components/geoblocking/index.tsx +++ b/src/typescript/frontend/src/components/geoblocking/index.tsx @@ -2,25 +2,33 @@ import React from "react"; import Text from "components/text"; import Carousel from "components/carousel"; +import useIsUserGeoblocked from "@hooks/use-is-user-geoblocked"; -export const GeoblockedBanner: React.FC = () => ( -
- -
- - You are accessing our products and services from a restricted jurisdiction. We do not - allow access from certain jurisdictions including locations subject to sanctions - restrictions and other jurisdictions where our services are ineligible for use. For more - information, see our Terms of Use. - +export const GeoblockedBanner = () => { + // Don't show the banner unless `geoblocked` is explicitly true, not just true or undefined. + const geoblocked = useIsUserGeoblocked({ explicitlyGeoblocked: true }); + + return ( + geoblocked && ( +
+ +
+ + You are accessing our products and services from a restricted jurisdiction. We do not + allow access from certain jurisdictions including locations subject to sanctions + restrictions and other jurisdictions where our services are ineligible for use. For + more information, see our Terms of Use. + +
+
- -
-); + ) + ); +}; diff --git a/src/typescript/frontend/src/components/header/components/mobile-menu/index.tsx b/src/typescript/frontend/src/components/header/components/mobile-menu/index.tsx index a39d0e96f..e7883fa40 100644 --- a/src/typescript/frontend/src/components/header/components/mobile-menu/index.tsx +++ b/src/typescript/frontend/src/components/header/components/mobile-menu/index.tsx @@ -1,12 +1,9 @@ import React, { useCallback, useEffect, useState } from "react"; - import { MobileMenuInner, MobileMenuWrapper, StyledMotion } from "./styled"; import { EXTERNAL_LINK_PROPS, Link } from "components/link"; import { MobileSocialLinks } from "./components/mobile-social-links"; import { MobileMenuItem } from "../index"; - import { type MobileMenuProps } from "./types"; - import { slideVariants } from "./animations"; import ButtonWithConnectWalletFallback from "components/header/wallet-button/ConnectWalletButton"; import { useAptos } from "context/wallet-context/AptosContextProvider"; @@ -26,7 +23,6 @@ export const MobileMenu: React.FC = ({ isOpen, setIsOpen, linksForCurrentPage, - geoblocked, }) => { const { wallet, account, disconnect } = useWallet(); const { copyAddress } = useAptos(); @@ -96,7 +92,6 @@ export const MobileMenu: React.FC = ({ void; linksForCurrentPage: (typeof NAVIGATE_LINKS)[number][]; - geoblocked: boolean; } diff --git a/src/typescript/frontend/src/components/header/index.tsx b/src/typescript/frontend/src/components/header/index.tsx index e45bfde95..ac011e6a5 100644 --- a/src/typescript/frontend/src/components/header/index.tsx +++ b/src/typescript/frontend/src/components/header/index.tsx @@ -21,7 +21,7 @@ import Link, { type LinkProps } from "next/link"; import { useEmojiPicker } from "context/emoji-picker-context"; import { GeoblockedBanner } from "components/geoblocking"; -const Header: React.FC = ({ isOpen, setIsOpen, geoblocked }) => { +const Header = ({ isOpen, setIsOpen }: HeaderProps) => { const { isDesktop } = useMatchBreakpoints(); const { t } = translationFunction(); const searchParams = useSearchParams(); @@ -96,7 +96,7 @@ const Header: React.FC = ({ isOpen, setIsOpen, geoblocked }) => { ); })} - + @@ -109,13 +109,8 @@ const Header: React.FC = ({ isOpen, setIsOpen, geoblocked }) => { )} - - {geoblocked && } + + ); }; diff --git a/src/typescript/frontend/src/components/header/types.ts b/src/typescript/frontend/src/components/header/types.ts index 23173bfa8..8c38fffcd 100644 --- a/src/typescript/frontend/src/components/header/types.ts +++ b/src/typescript/frontend/src/components/header/types.ts @@ -1,5 +1,4 @@ export type HeaderProps = { isOpen: boolean; setIsOpen: (arg: boolean) => void; - geoblocked: boolean; }; diff --git a/src/typescript/frontend/src/components/header/wallet-button/ConnectWalletButton.tsx b/src/typescript/frontend/src/components/header/wallet-button/ConnectWalletButton.tsx index 6c0bb3768..adefbb941 100644 --- a/src/typescript/frontend/src/components/header/wallet-button/ConnectWalletButton.tsx +++ b/src/typescript/frontend/src/components/header/wallet-button/ConnectWalletButton.tsx @@ -10,12 +10,13 @@ import Arrow from "@icons/Arrow"; import { useNameStore } from "context/event-store-context"; import Popup from "components/popup"; import Text from "components/text"; +import useIsUserGeoblocked from "@hooks/use-is-user-geoblocked"; export interface ConnectWalletProps extends PropsWithChildren<{ className?: string }> { mobile?: boolean; onClick?: () => void; - geoblocked: boolean; arrow?: boolean; + forceAllowConnect?: boolean; } const CONNECT_WALLET = "Connect Wallet"; @@ -25,12 +26,19 @@ export const ButtonWithConnectWalletFallback: React.FC = ({ children, className, onClick, - geoblocked, arrow = true, + forceAllowConnect, }) => { const { connected, account } = useWallet(); const { openWalletModal } = useWalletModal(); const { t } = translationFunction(); + const shouldBeGeoblocked = useIsUserGeoblocked(); + const geoblocked = useMemo(() => { + // For letting the user connect on the `/verify_status` page when `forceAllowConnect` is `true`, + // by only returning `geoblocked = true` if we're not force allow connecting and they're + // geoblocked. + return !forceAllowConnect && shouldBeGeoblocked; + }, [forceAllowConnect, shouldBeGeoblocked]); const [enabled, setEnabled] = useState(false); diff --git a/src/typescript/frontend/src/components/inputs/search-bar.tsx b/src/typescript/frontend/src/components/inputs/search-bar.tsx index c6ee2b327..b0bf187dc 100644 --- a/src/typescript/frontend/src/components/inputs/search-bar.tsx +++ b/src/typescript/frontend/src/components/inputs/search-bar.tsx @@ -36,7 +36,7 @@ export const Border = styled(Flex)` } `; -export const SearchBar: React.FC<{ geoblocked: boolean }> = ({ geoblocked }) => { +export const SearchBar = () => { const setMode = useEmojiPicker((state) => state.setMode); useEffect(() => { setMode("search"); @@ -63,7 +63,6 @@ export const SearchBar: React.FC<{ geoblocked: boolean }> = ({ geoblocked }) => {}} inputClassName="search-picker border-none" - geoblocked={geoblocked} filterEmojis={filterBigEmojis} /> diff --git a/src/typescript/frontend/src/components/pages/emojicoin/ClientEmojicoinPage.tsx b/src/typescript/frontend/src/components/pages/emojicoin/ClientEmojicoinPage.tsx index 382828666..3c35b5ca3 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/ClientEmojicoinPage.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/ClientEmojicoinPage.tsx @@ -37,11 +37,7 @@ const ClientEmojicoinPage = (props: EmojicoinProps) => { - {isTablet || isMobile ? ( - - ) : ( - - )} + {isTablet || isMobile ? : } ); }; diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/chat/ChatBox.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/chat/ChatBox.tsx index de32f79ba..50534cd54 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/chat/ChatBox.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/chat/ChatBox.tsx @@ -111,7 +111,6 @@ const ChatBox = (props: ChatProps) => { > {sortedChatsWithNames.map((chat, index) => { const message = { - // TODO: Resolve address to Aptos name, store in state. sender: chat.user, text: chat.message, senderRank: getRankFromEvent(chat).rankIcon, @@ -129,7 +128,7 @@ const ChatBox = (props: ChatProps) => { - + ); }; diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/desktop-grid/index.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/desktop-grid/index.tsx index 28c7a3f80..175a3276a 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/desktop-grid/index.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/desktop-grid/index.tsx @@ -44,7 +44,7 @@ const DesktopGrid = (props: GridProps) => { - + { marketAddress={props.data.marketView.metadata.marketAddress} marketEmojis={props.data.symbolEmojis} initNumSwaps={props.data.swaps.length} - geoblocked={props.geoblocked} /> @@ -79,7 +78,7 @@ const DesktopGrid = (props: GridProps) => { - + diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/mobile-grid/index.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/mobile-grid/index.tsx index e873a1368..372ad3513 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/mobile-grid/index.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/mobile-grid/index.tsx @@ -84,12 +84,11 @@ const MobileGrid = (props: GridProps) => { ) : tab === 2 ? ( <>
- +
{ ) : ( - + )} diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/LiquidityButton.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/LiquidityButton.tsx index 120ce4ec5..c6b5c1f2a 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/LiquidityButton.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/LiquidityButton.tsx @@ -33,7 +33,7 @@ export const LiquidityButton = (props: GridProps) => { ) : canTrade ? ( - + ) : ( diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapButton.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapButton.tsx index 0c95cc109..79a3f3a3d 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapButton.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapButton.tsx @@ -26,7 +26,6 @@ export const SwapButton = ({ marketAddress, setSubmit, disabled, - geoblocked, symbol, minOutputAmount, }: { @@ -35,7 +34,6 @@ export const SwapButton = ({ marketAddress: AccountAddressString; setSubmit: Dispatch Promise) | null>>; disabled?: boolean; - geoblocked: boolean; symbol: string; minOutputAmount: bigint | number | string; }) => { @@ -100,7 +98,7 @@ export const SwapButton = ({ return ( <> - + {canTrade ? ( <>
diff --git a/src/typescript/frontend/src/components/pages/launch-emojicoin/memoized-launch/components/launch-or-goto.tsx b/src/typescript/frontend/src/components/pages/launch-emojicoin/memoized-launch/components/launch-or-goto.tsx index 10848f948..67ff98382 100644 --- a/src/typescript/frontend/src/components/pages/launch-emojicoin/memoized-launch/components/launch-or-goto.tsx +++ b/src/typescript/frontend/src/components/pages/launch-emojicoin/memoized-launch/components/launch-or-goto.tsx @@ -10,12 +10,10 @@ export const LaunchButtonOrGoToMarketLink = ({ onWalletButtonClick, registered, invalid, - geoblocked, }: { onWalletButtonClick: () => void; registered?: boolean; invalid: boolean; - geoblocked: boolean; }) => { const emojis = useEmojiPicker((state) => state.emojis); const { t } = translationFunction(); @@ -27,7 +25,7 @@ export const LaunchButtonOrGoToMarketLink = ({ return ( <> - + {registered ? ( { - // Maybe it's this...? Maybe we need to memoize this value. +export const MemoizedLaunchAnimation = ({ loading }: { loading: boolean }) => { const { t } = translationFunction(); const emojis = useEmojiPicker((state) => state.emojis); const setIsLoadingRegisteredMarket = useEmojiPicker( @@ -93,7 +86,6 @@ export const MemoizedLaunchAnimation = ({
{ diff --git a/src/typescript/frontend/src/components/pages/pools/ClientPoolsPage.tsx b/src/typescript/frontend/src/components/pages/pools/ClientPoolsPage.tsx index 2273c3b59..c407639c1 100644 --- a/src/typescript/frontend/src/components/pages/pools/ClientPoolsPage.tsx +++ b/src/typescript/frontend/src/components/pages/pools/ClientPoolsPage.tsx @@ -24,10 +24,7 @@ import { type MarketStateModel, type UserPoolsRPCModel } from "@sdk/indexer-v2/t export type PoolsData = MarketStateModel | UserPoolsRPCModel; -export const ClientPoolsPage: React.FC<{ geoblocked: boolean; initialData: PoolsData[] }> = ({ - geoblocked, - initialData, -}) => { +export const ClientPoolsPage = ({ initialData }: { initialData: PoolsData[] }) => { const searchParams = useSearchParams(); const poolParam = searchParams.get("pool"); const [sortBy, setSortBy] = useState("all_time_vol"); @@ -91,7 +88,7 @@ export const ClientPoolsPage: React.FC<{ geoblocked: boolean; initialData: Pools alignItems="center" gap="13px" > - {!isMobile ? : null} + {!isMobile ? : null} - + ) : null} @@ -142,10 +139,7 @@ export const ClientPoolsPage: React.FC<{ geoblocked: boolean; initialData: Pools - + diff --git a/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx b/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx index e44a099dc..38c7d80e4 100644 --- a/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx +++ b/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx @@ -32,7 +32,6 @@ import { EmojiPill } from "components/EmojiPill"; type LiquidityProps = { market: PoolsData | undefined; - geoblocked: boolean; }; const fmtCoin = (n: AnyNumberString | undefined) => { @@ -69,7 +68,7 @@ const inputAndOutputStyles = ` border-transparent !p-0 text-white `; -const Liquidity: React.FC = ({ market, geoblocked }) => { +const Liquidity = ({ market }: LiquidityProps) => { const { t } = translationFunction(); const { theme } = useThemeContext(); @@ -348,7 +347,7 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { mb={{ _: "17px", tablet: "37px" }} position="relative" > - + + + ) : ( +
+ +
+ )} +
+ ); +}; + +export default BondingProgress; diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/main-info/MainInfo.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/main-info/MainInfo.tsx index f39064d48..249820e20 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/main-info/MainInfo.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/main-info/MainInfo.tsx @@ -4,21 +4,19 @@ import { translationFunction } from "context/language-context"; import { toCoinDecimalString } from "lib/utils/decimals"; import AptosIconBlack from "components/svg/icons/AptosBlack"; import { type MainInfoProps } from "../../types"; -import { emojisToName } from "lib/utils/emojis-to-name-or-symbol"; import { useEventStore } from "context/event-store-context"; import { useLabelScrambler } from "components/pages/home/components/table-card/animation-variants/event-variants"; import { isMarketStateModel } from "@sdk/indexer-v2/types"; +import BondingProgress from "./BondingProgress"; +import { useThemeContext } from "context"; +import { useMatchBreakpoints } from "@hooks/index"; import { Emoji } from "utils/emoji"; -const innerWrapper = `flex flex-col md:flex-row justify-around w-full max-w-[1362px] px-[30px] lg:px-[44px] py-[17px] -md:py-[37px] xl:py-[68px]`; -const headerWrapper = - "flex flex-row md:flex-col md:justify-between gap-[12px] md:gap-[4px] w-full md:w-[58%] xl:w-[65%] mb-[8px]"; -const statsWrapper = "flex flex-col w-full md:w-[42%] xl:w-[35%] mt-[-8px]"; -const statsTextClasses = "display-6 md:display-4 uppercase ellipses font-forma"; +const statsTextClasses = "uppercase ellipses font-forma text-[24px]"; const MainInfo = ({ data }: MainInfoProps) => { const { t } = translationFunction(); + const { theme } = useThemeContext(); const marketEmojis = data.symbolEmojis; const stateEvents = useEventStore((s) => s.getMarket(marketEmojis)?.stateEvents ?? []); @@ -41,27 +39,48 @@ const MainInfo = ({ data }: MainInfoProps) => { } }, [stateEvents]); - const { ref: marketCapRef } = useLabelScrambler(marketCap); - const { ref: dailyVolumeRef } = useLabelScrambler(dailyVolume); - const { ref: allTimeVolumeRef } = useLabelScrambler(allTimeVolume); + const { ref: marketCapRef } = useLabelScrambler(toCoinDecimalString(marketCap, 2)); + const { ref: dailyVolumeRef } = useLabelScrambler(toCoinDecimalString(dailyVolume, 2)); + const { ref: allTimeVolumeRef } = useLabelScrambler(toCoinDecimalString(allTimeVolume, 2)); - return ( -
-
-
-
- {emojisToName(data.emojis)} -
+ const { isMobile } = useMatchBreakpoints(); - -
+ return ( +
+
+ -
-
-
{t("Mkt. Cap:")}
+
+
+
{t("Market Cap:")}
{toCoinDecimalString(marketCap, 2)}
@@ -71,7 +90,7 @@ const MainInfo = ({ data }: MainInfoProps) => {
-
+
{t("24 hour vol:")}
@@ -82,7 +101,7 @@ const MainInfo = ({ data }: MainInfoProps) => {
-
+
{t("All-time vol:")}
@@ -92,6 +111,10 @@ const MainInfo = ({ data }: MainInfoProps) => {
+ +
+ +
diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx index a05aa5ee9..70588e749 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx @@ -10,7 +10,7 @@ import { toActualCoinDecimals, toDisplayCoinDecimals } from "lib/utils/decimals" import { useScramble } from "use-scramble"; import { useSimulateSwap } from "lib/hooks/queries/use-simulate-swap"; import { useEventStore } from "context/event-store-context"; -import { useMatchBreakpoints, useTooltip } from "@hooks/index"; +import { useTooltip } from "@hooks/index"; import { useSearchParams } from "next/navigation"; import { translationFunction } from "context/language-context"; import { useAptos } from "context/wallet-context/AptosContextProvider"; @@ -69,7 +69,6 @@ export default function SwapComponent({ presetInputAmount !== null && presetInputAmount !== "" && !Number.isNaN(Number(presetInputAmount)); - const { isDesktop } = useMatchBreakpoints(); const [inputAmount, setInputAmount] = useState( toActualCoinDecimals({ num: presetInputAmountIsValid ? presetInputAmount! : "1" }) ); @@ -177,7 +176,7 @@ export default function SwapComponent({ const { theme } = useThemeContext(); - const { targetRef, tooltip } = useTooltip( + const { targetRef, tooltip: gearTooltip } = useTooltip( setMaxSlippage(getMaxSlippageSettings().maxSlippage)} />, @@ -185,141 +184,141 @@ export default function SwapComponent({ placement: "bottom", customStyles: getTooltipStyles(theme), trigger: "click", + tooltipOffset: [100, 10], } ); return ( - <> - - -
- {t("Trade Emojicoin")} -
- - {isSell ? ( - <> - { - setInputAmount(emojicoinBalance / 2n); - }} - /> - { - setInputAmount(emojicoinBalance); - }} - /> - - ) : ( - <> - { - setInputAmount(availableAptBalance / 4n); - }} - /> - { - setInputAmount(availableAptBalance / 2n); - }} - /> - { - setInputAmount(availableAptBalance); - }} - /> - - )} - -
- - - - -
- {isSell ? t("You sell") : t("You pay")} - {balanceLabel} -
- setInputAmount(v)} - onSubmit={() => (submit ? submit() : {})} - decimals={8} + + +
+ +
+ {gearTooltip} + + {isSell ? ( + <> + { + setInputAmount(emojicoinBalance / 2n); + }} + /> + { + setInputAmount(emojicoinBalance); + }} /> -
- {isSell ? : } -
+ + ) : ( + <> + { + setInputAmount(availableAptBalance / 4n); + }} + /> + { + setInputAmount(availableAptBalance / 2n); + }} + /> + { + setInputAmount(availableAptBalance); + }} + /> + + )} + + + + + + +
+ {isSell ? t("You sell") : t("You pay")} + {balanceLabel} +
+ setInputAmount(v)} + onSubmit={() => (submit ? submit() : {})} + decimals={8} + /> +
+ {isSell ? : } +
- { - setInputAmount(outputAmount); - // This is done as to not display an old value if the swap simulation fails. - setOutputAmount(0n); - setPrevious(0n); - setIsSell((v) => !v); - }} - /> + { + setInputAmount(outputAmount); + // This is done as to not display an old value if the swap simulation fails. + setOutputAmount(0n); + setPrevious(0n); + setIsSell((v) => !v); + }} + /> - - -
{t("You receive")}
-
-
setIsSell((v) => !v)} - className={inputAndOutputStyles + " mt-[8px] ml-[1px] cursor-pointer"} - style={{ opacity: isLoading ? 0.6 : 1 }} - > - {/* Scrambled swap result output below. */} -
-
+ + +
{t("You receive")}
+
+
setIsSell((v) => !v)} + className={inputAndOutputStyles + " mt-[8px] ml-[1px] cursor-pointer"} + style={{ opacity: isLoading ? 0.6 : 1 }} + > + {/* Scrambled swap result output below. */} +
- - {isSell ? : } - - -
-
- -
- {tooltip} -
- - {gasCost === null ? "~" : ""} - {toDisplayCoinDecimals({ - num: gasCost !== null ? gasCost.toString() : SWAP_GAS_COST.toString(), - decimals: 4, - })}{" "} - APT - {" "} - -
+
+ + {isSell ? : } + + +
+
+
+ + {gasCost === null ? "~" : ""} + {toDisplayCoinDecimals({ + num: gasCost !== null ? gasCost.toString() : SWAP_GAS_COST.toString(), + decimals: 4, + })}{" "} + APT + {" "} +
+
- - - - - + + + + ); } diff --git a/src/typescript/frontend/src/components/pages/home/components/main-card/MainCard.tsx b/src/typescript/frontend/src/components/pages/home/components/main-card/MainCard.tsx index 53b436073..f595f77a2 100644 --- a/src/typescript/frontend/src/components/pages/home/components/main-card/MainCard.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/main-card/MainCard.tsx @@ -51,9 +51,9 @@ const MainCard = (props: MainCardProps) => { }, []); /* eslint-enable react-hooks/exhaustive-deps */ - const { ref: marketCapRef } = useLabelScrambler(marketCap); - const { ref: dailyVolumeRef } = useLabelScrambler(dailyVolume); - const { ref: allTimeVolumeRef } = useLabelScrambler(allTimeVolume); + const { ref: marketCapRef } = useLabelScrambler(toCoinDecimalString(marketCap, 2)); + const { ref: dailyVolumeRef } = useLabelScrambler(toCoinDecimalString(dailyVolume, 2)); + const { ref: allTimeVolumeRef } = useLabelScrambler(toCoinDecimalString(allTimeVolume, 2)); return ( diff --git a/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx b/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx index 133cece0a..9110d977d 100644 --- a/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx @@ -103,8 +103,14 @@ const TableCard = ({ /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [animations, runAnimationSequence]); - const { ref: marketCapRef } = useLabelScrambler(marketCap, " APT"); - const { ref: dailyVolumeRef } = useLabelScrambler(dailyVolume, " APT"); + const { ref: marketCapRef } = useLabelScrambler( + toCoinDecimalString(marketCap.toString(), 2), + " APT" + ); + const { ref: dailyVolumeRef } = useLabelScrambler( + toCoinDecimalString(dailyVolume.toString(), 2), + " APT" + ); const { curr, prev, variant, displayIndex, layoutDelay } = useMemo(() => { const { curr, prev } = calculateGridData({ diff --git a/src/typescript/frontend/src/components/pages/home/components/table-card/animation-variants/event-variants.ts b/src/typescript/frontend/src/components/pages/home/components/table-card/animation-variants/event-variants.ts index 25cf953c2..e0871b6ec 100644 --- a/src/typescript/frontend/src/components/pages/home/components/table-card/animation-variants/event-variants.ts +++ b/src/typescript/frontend/src/components/pages/home/components/table-card/animation-variants/event-variants.ts @@ -1,4 +1,3 @@ -import { type AnyNumberString } from "@sdk-types"; import { Trigger } from "@sdk/const"; import { type ChatEventModel, @@ -12,8 +11,6 @@ import { type MarketRegistrationEventModel, type SwapEventModel, } from "@sdk/indexer-v2/types"; -import type Big from "big.js"; -import { toCoinDecimalString } from "lib/utils/decimals"; import { ECONIA_BLUE, GREEN, PINK, WHITE } from "theme/colors"; import { useScramble } from "use-scramble"; @@ -130,10 +127,10 @@ export const scrambleConfig = { overdrive: false, overflow: true, speed: 0.6, - playOnMount: false, + playOnMount: true, }; -export const useLabelScrambler = (value: AnyNumberString | Big, suffix: string = "") => { +export const useLabelScrambler = (value: string, suffix: string = "") => { // Ignore all characters in the suffix, as long as they are not numbers. const ignore = ["."]; const numberSet = new Set("0123456789"); @@ -145,7 +142,7 @@ export const useLabelScrambler = (value: AnyNumberString | Big, suffix: string = } const scrambler = useScramble({ - text: toCoinDecimalString(value.toString(), 2) + suffix, + text: value, ...scrambleConfig, ignore, }); diff --git a/src/typescript/frontend/src/components/selects/trade-options/index.tsx b/src/typescript/frontend/src/components/selects/trade-options/index.tsx index 71aaf2d97..436d6d497 100644 --- a/src/typescript/frontend/src/components/selects/trade-options/index.tsx +++ b/src/typescript/frontend/src/components/selects/trade-options/index.tsx @@ -65,7 +65,7 @@ export const TradeOptions = ({ onMaxSlippageUpdate }: TradeOptionsProps) => {
{ decimals={2} className="w-[4rem] bg-transparent text-right outline-none" /> - % + %
diff --git a/src/typescript/frontend/src/utils/bonding-curve.ts b/src/typescript/frontend/src/utils/bonding-curve.ts index 7396e0101..4f2389cbc 100644 --- a/src/typescript/frontend/src/utils/bonding-curve.ts +++ b/src/typescript/frontend/src/utils/bonding-curve.ts @@ -50,6 +50,7 @@ export const isInBondingCurve = ( * @returns the percentage of the bonding curve progress */ export const getBondingCurveProgress = (clammVirtualReservesQuote: number | bigint) => { + if (BigInt(clammVirtualReservesQuote) === 0n) return 100; return Big(clammVirtualReservesQuote.toString()) .sub(QUOTE_VIRTUAL_FLOOR.toString()) .div(QUOTE_REAL_CEILING.toString()) From b5f7b276d9d009cc578b5cd69db131391fe8d315 Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:36:31 -0800 Subject: [PATCH 36/94] [ECO-2360] Add claim link module to rewards package (#320) --- src/move/rewards/Move.toml | 5 +- src/move/rewards/README.md | 10 +- .../sources/emojicoin_dot_fun_claim_link.move | 656 ++++++++++++++++++ .../sources/emojicoin_dot_fun_rewards.move | 169 ++--- 4 files changed, 759 insertions(+), 81 deletions(-) create mode 100644 src/move/rewards/sources/emojicoin_dot_fun_claim_link.move diff --git a/src/move/rewards/Move.toml b/src/move/rewards/Move.toml index 998e5be3e..08e52d06e 100644 --- a/src/move/rewards/Move.toml +++ b/src/move/rewards/Move.toml @@ -11,8 +11,11 @@ emojicoin_dot_fun = "0xc0de" integrator = "0xccc" rewards = "0xddd" +[dev-dependencies.BlackHeartCoinFactory] +local = "../test_coin_factories/black_heart" + [package] authors = ["Econia Labs (developers@econialabs.com)"] name = "EmojicoinDotFunRewards" upgrade_policy = "compatible" -version = "1.0.1" +version = "1.1.0" diff --git a/src/move/rewards/README.md b/src/move/rewards/README.md index aef2f8758..95d7de08e 100644 --- a/src/move/rewards/README.md +++ b/src/move/rewards/README.md @@ -39,11 +39,12 @@ aptos move publish \ --profile $PROFILE ``` -## Fund the vault +## Fund the vaults ```sh REWARDS=0xaaa... PROFILE=my-profile +N_CLAIM_LINK_REDEMPTIONS_TO_FUND=10 N_REWARDS_TO_FUND_PER_TIER="u64:[1500,500,200,50,5,1]" ``` @@ -53,3 +54,10 @@ aptos move run \ --function-id $REWARDS::emojicoin_dot_fun_rewards::fund_tiers \ --profile $PROFILE ``` + +```sh +aptos move run \ + --args $N_CLAIM_LINK_REDEMPTIONS_TO_FUND \ + --function-id $REWARDS::emojicoin_dot_fun_claim_link::fund_vault \ + --profile $PROFILE +``` diff --git a/src/move/rewards/sources/emojicoin_dot_fun_claim_link.move b/src/move/rewards/sources/emojicoin_dot_fun_claim_link.move new file mode 100644 index 000000000..031187360 --- /dev/null +++ b/src/move/rewards/sources/emojicoin_dot_fun_claim_link.move @@ -0,0 +1,656 @@ +// cspell:word funder +// cspell:word unvalidated +module rewards::emojicoin_dot_fun_claim_link { + + use aptos_framework::account::{Self, SignerCapability}; + use aptos_framework::aptos_account; + use aptos_framework::aptos_coin::AptosCoin; + use aptos_framework::coin; + use aptos_framework::event; + use aptos_std::ed25519::{Self, ValidatedPublicKey}; + use aptos_std::simple_map::SimpleMap; + use aptos_std::smart_table::{Self, SmartTable}; + use emojicoin_dot_fun::emojicoin_dot_fun::{Self, Swap}; + use std::option::{Self, Option}; + use std::bcs; + use std::signer; + + const INTEGRATOR_FEE_RATE_BPS: u8 = 100; + const NIL: address = @0x0; + const DEFAULT_CLAIM_AMOUNT: u64 = 100_000_000; + const VAULT: vector = b"Claim link vault"; + + /// Signer does not correspond to admin. + const E_NOT_ADMIN: u64 = 0; + /// Admin to remove address does not correspond to admin. + const E_ADMIN_TO_REMOVE_IS_NOT_ADMIN: u64 = 1; + /// Public key of claim link private key is not in manifest. + const E_INVALID_CLAIM_LINK: u64 = 2; + /// Claim link has already been claimed. + const E_CLAIM_LINK_ALREADY_CLAIMED: u64 = 3; + /// Vault has insufficient funds. + const E_VAULT_INSUFFICIENT_FUNDS: u64 = 4; + /// Public key does not pass Ed25519 validation. + const E_INVALID_PUBLIC_KEY: u64 = 5; + /// Signature does not pass Ed25519 validation. + const E_INVALID_SIGNATURE: u64 = 6; + /// Admin to remove is rewards publisher. + const E_ADMIN_TO_REMOVE_IS_REWARDS_PUBLISHER: u64 = 7; + /// Admin is already an admin. + const E_ALREADY_ADMIN: u64 = 8; + + struct Vault has key { + /// Addresses of signers who can mutate the manifest. + admins: vector
, + /// In octas. + claim_amount: u64, + /// Map from claim link public key to address of claimant, `NIL` if unclaimed. + manifest: SmartTable, + /// Approves transfers from the vault. + signer_capability: SignerCapability + } + + #[event] + struct EmojicoinDotFunClaimLinkRedemption has copy, drop, store { + claimant: address, + claim_amount: u64, + swap: Swap + } + + #[view] + public fun admins(): vector
acquires Vault { + Vault[@rewards].admins + } + + #[view] + public fun claim_amount(): u64 acquires Vault { + Vault[@rewards].claim_amount + } + + #[view] + public fun public_key_is_in_manifest(public_key_bytes: vector): bool acquires Vault { + Vault[@rewards].manifest.contains(validate_public_key_bytes(public_key_bytes)) + } + + #[view] + public fun public_key_claimant(public_key_bytes: vector): address acquires Vault { + *Vault[@rewards].manifest.borrow(validate_public_key_bytes(public_key_bytes)) + } + + #[view] + public fun public_keys(): vector acquires Vault { + Vault[@rewards].manifest.keys() + } + + #[view] + public fun public_keys_paginated( + starting_bucket_index: u64, starting_vector_index: u64, num_public_keys_to_get: u64 + ): (vector, Option, Option) acquires Vault { + Vault[@rewards].manifest.keys_paginated( + starting_bucket_index, starting_vector_index, num_public_keys_to_get + ) + } + + #[view] + public fun manifest_to_simple_map(): SimpleMap acquires Vault { + Vault[@rewards].manifest.to_simple_map() + } + + #[view] + public fun vault_balance(): u64 acquires Vault { + coin::balance( + account::get_signer_capability_address(&Vault[@rewards].signer_capability) + ) + } + + #[view] + public fun vault_signer_address(): address acquires Vault { + account::get_signer_capability_address(&Vault[@rewards].signer_capability) + } + + public entry fun add_admin(admin: &signer, new_admin: address) acquires Vault { + let admins_ref_mut = &mut borrow_vault_mut_checked(admin).admins; + assert!(!admins_ref_mut.contains(&new_admin), E_ALREADY_ADMIN); + admins_ref_mut.push_back(new_admin); + } + + public entry fun add_public_keys( + admin: &signer, public_keys_as_bytes: vector> + ) acquires Vault { + let manifest_ref_mut = &mut borrow_vault_mut_checked(admin).manifest; + public_keys_as_bytes.for_each(|public_key_bytes| { + manifest_ref_mut.add(validate_public_key_bytes(public_key_bytes), NIL); + }); + } + + public entry fun fund_vault(funder: &signer, n_claims: u64) acquires Vault { + let amount = n_claims * Vault[@rewards].claim_amount; + aptos_account::transfer( + funder, + account::get_signer_capability_address(&Vault[@rewards].signer_capability), + amount + ); + } + + /// `signature_bytes` is generated by signing the claimant's address with the claim link private + /// key, and can be verified by `public_key_bytes`, the corresponding claim link public key. + public entry fun redeem( + claimant: &signer, + signature_bytes: vector, + public_key_bytes: vector, + market_address: address, + min_output_amount: u64 + ) acquires Vault { + + // Verify signature. + let validated_public_key = validate_public_key_bytes(public_key_bytes); + let claimant_address = signer::address_of(claimant); + assert!( + ed25519::signature_verify_strict( + &ed25519::new_signature_from_bytes(signature_bytes), + &ed25519::public_key_to_unvalidated(&validated_public_key), + bcs::to_bytes(&claimant_address) + ), + E_INVALID_SIGNATURE + ); + + // Verify public key is eligible for claim. + let vault_ref_mut = &mut Vault[@rewards]; + let manifest_ref_mut = &mut vault_ref_mut.manifest; + assert!(manifest_ref_mut.contains(validated_public_key), E_INVALID_CLAIM_LINK); + assert!( + *manifest_ref_mut.borrow(validated_public_key) == NIL, + E_CLAIM_LINK_ALREADY_CLAIMED + ); + + // Check vault balance. + let vault_signer_cap_ref = &vault_ref_mut.signer_capability; + let vault_address = account::get_signer_capability_address(vault_signer_cap_ref); + let claim_amount = vault_ref_mut.claim_amount; + assert!( + coin::balance(vault_address) >= claim_amount, + E_VAULT_INSUFFICIENT_FUNDS + ); + + // Update manifest, transfer APT to claimant. + *manifest_ref_mut.borrow_mut(validated_public_key) = claimant_address; + let vault_signer = account::create_signer_with_capability(vault_signer_cap_ref); + aptos_account::transfer(&vault_signer, claimant_address, claim_amount); + + // Invoke swap, emit event. + let swap_event = + emojicoin_dot_fun::simulate_swap( + claimant_address, + market_address, + claim_amount, + false, + @integrator, + INTEGRATOR_FEE_RATE_BPS + ); + emojicoin_dot_fun::swap( + claimant, + market_address, + claim_amount, + false, + @integrator, + INTEGRATOR_FEE_RATE_BPS, + min_output_amount + ); + event::emit( + EmojicoinDotFunClaimLinkRedemption { + claimant: claimant_address, + claim_amount, + swap: swap_event + } + ); + + } + + public entry fun remove_admin(admin: &signer, admin_to_remove: address) acquires Vault { + let admins_ref_mut = &mut borrow_vault_mut_checked(admin).admins; + let (admin_to_remove_is_admin, admin_to_remove_index) = + admins_ref_mut.index_of(&admin_to_remove); + assert!(admin_to_remove != @rewards, E_ADMIN_TO_REMOVE_IS_REWARDS_PUBLISHER); + assert!(admin_to_remove_is_admin, E_ADMIN_TO_REMOVE_IS_NOT_ADMIN); + admins_ref_mut.remove(admin_to_remove_index); + } + + public entry fun remove_public_keys( + admin: &signer, public_keys_as_bytes: vector> + ) acquires Vault { + let manifest_ref_mut = &mut borrow_vault_mut_checked(admin).manifest; + public_keys_as_bytes.for_each(|public_key_bytes| { + let validated_public_key = validate_public_key_bytes(public_key_bytes); + if (manifest_ref_mut.contains(validated_public_key) + && *manifest_ref_mut.borrow(validated_public_key) == NIL) { + manifest_ref_mut.remove(validated_public_key); + } + }); + } + + public entry fun set_claim_amount(admin: &signer, claim_amount: u64) acquires Vault { + borrow_vault_mut_checked(admin).claim_amount = claim_amount; + } + + public entry fun withdraw_from_vault(admin: &signer, amount: u64) acquires Vault { + aptos_account::transfer( + &account::create_signer_with_capability( + &borrow_vault_mut_checked(admin).signer_capability + ), + signer::address_of(admin), + amount + ); + } + + fun init_module(rewards: &signer) { + let (vault_signer, signer_capability) = + account::create_resource_account(rewards, VAULT); + move_to( + rewards, + Vault { + admins: vector[signer::address_of(rewards)], + claim_amount: DEFAULT_CLAIM_AMOUNT, + manifest: smart_table::new(), + signer_capability + } + ); + coin::register(&vault_signer); + } + + inline fun borrow_vault_mut_checked(admin: &signer): &mut Vault { + let vault_ref_mut = &mut Vault[@rewards]; + assert!(vault_ref_mut.admins.contains(&signer::address_of(admin)), E_NOT_ADMIN); + vault_ref_mut + } + + inline fun validate_public_key_bytes(public_key_bytes: vector): ValidatedPublicKey { + let validated_public_key_option = + ed25519::new_validated_public_key_from_bytes(public_key_bytes); + assert!(option::is_some(&validated_public_key_option), E_INVALID_PUBLIC_KEY); + option::destroy_some(validated_public_key_option) + } + + #[test_only] + use aptos_framework::account::{create_signer_for_test as get_signer}; + #[test_only] + use black_cat_market::coin_factory::{ + Emojicoin as BlackCatEmojicoin, + EmojicoinLP as BlackCatEmojicoinLP + }; + + #[test_only] + const CLAIMANT: address = @0x1111; + + #[test_only] + fun prepare_for_redemption(): (vector, vector) acquires Vault { + // Init package, execute exact transition swap, fund vault. + emojicoin_dot_fun::tests::init_package_then_exact_transition(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + emojicoin_dot_fun::test_acquisitions::mint_aptos_coin_to( + @rewards, DEFAULT_CLAIM_AMOUNT + ); + fund_vault(&rewards_signer, 1); + + // Generate private, public keys. + let (claim_link_private_key, claim_link_validated_public_key) = + ed25519::generate_keys(); + let claim_link_validated_public_key_bytes = + ed25519::validated_public_key_to_bytes(&claim_link_validated_public_key); + let signature_bytes = + ed25519::signature_to_bytes( + &ed25519::sign_arbitrary_bytes( + &claim_link_private_key, bcs::to_bytes(&CLAIMANT) + ) + ); + add_public_keys( + &rewards_signer, + vector[claim_link_validated_public_key_bytes] + ); + + // Return valid signature against claimant's address, claim link public key bytes. + (signature_bytes, claim_link_validated_public_key_bytes) + } + + #[test, expected_failure(abort_code = E_ALREADY_ADMIN)] + fun test_add_admin_already_admin() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + add_admin(&rewards_signer, @rewards); + } + + #[test, expected_failure(abort_code = E_NOT_ADMIN)] + fun test_add_admin_not_admin() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + let not_admin = @0x2222; + let not_admin_signer = get_signer(not_admin); + assert!(&rewards_signer != ¬_admin_signer); + add_admin(¬_admin_signer, not_admin); + } + + #[test, expected_failure(abort_code = E_INVALID_PUBLIC_KEY)] + fun test_add_public_keys_invalid_public_key() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + add_public_keys(&rewards_signer, vector[vector[0x0]]); + } + + #[test, expected_failure(abort_code = E_NOT_ADMIN)] + fun test_add_public_keys_not_admin() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + let not_admin_signer = get_signer(@0x2222); + assert!(&rewards_signer != ¬_admin_signer); + add_public_keys(¬_admin_signer, vector[]); + } + + #[test] + fun test_general_flow() acquires Vault { + // Initialize black cat market, have it undergo state transition. + emojicoin_dot_fun::tests::init_package_then_exact_transition(); + + // Get claim link private, public keys. + let (claim_link_private_key, claim_link_validated_public_key) = + ed25519::generate_keys(); + let claim_link_validated_public_key_bytes = + ed25519::validated_public_key_to_bytes(&claim_link_validated_public_key); + + // Initialize module. + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + + // Check initial state. + assert!(admins() == vector[@rewards]); + assert!(claim_amount() == DEFAULT_CLAIM_AMOUNT); + assert!(!public_key_is_in_manifest(claim_link_validated_public_key_bytes)); + assert!(public_keys().is_empty()); + let (keys, starting_bucket_index, starting_vector_index) = + public_keys_paginated(0, 0, 1); + assert!(keys == vector[]); + assert!(starting_bucket_index == option::none()); + assert!(starting_vector_index == option::none()); + assert!(manifest_to_simple_map().length() == 0); + assert!(vault_balance() == 0); + assert!( + vault_signer_address() + == account::get_signer_capability_address( + &Vault[@rewards].signer_capability + ) + ); + + // Add an admin, public key, mint APT, fund vault. + add_public_keys( + &rewards_signer, + vector[claim_link_validated_public_key_bytes] + ); + emojicoin_dot_fun::test_acquisitions::mint_aptos_coin_to( + @rewards, DEFAULT_CLAIM_AMOUNT + ); + let n_redemptions = 1; + fund_vault(&rewards_signer, n_redemptions); + let new_admin = @0x2222; + add_admin(&rewards_signer, new_admin); + + // Check new state. + assert!(admins() == vector[@rewards, new_admin]); + assert!(claim_amount() == DEFAULT_CLAIM_AMOUNT); + assert!(public_key_is_in_manifest(claim_link_validated_public_key_bytes)); + assert!( + manifest_to_simple_map().keys() == vector[claim_link_validated_public_key] + ); + assert!(public_key_claimant(claim_link_validated_public_key_bytes) == NIL); + (keys, starting_bucket_index, starting_vector_index) = public_keys_paginated( + 0, 0, 1 + ); + assert!(keys == vector[claim_link_validated_public_key]); + assert!(starting_bucket_index == option::none()); + assert!(starting_vector_index == option::none()); + assert!( + manifest_to_simple_map().keys() == vector[claim_link_validated_public_key] + ); + assert!(vault_balance() == DEFAULT_CLAIM_AMOUNT); + + // Fund another reward, double claim amount, fund another reward, remove admin, withdraw. + emojicoin_dot_fun::test_acquisitions::mint_aptos_coin_to( + @rewards, 3 * DEFAULT_CLAIM_AMOUNT + ); + fund_vault(&rewards_signer, 1); + set_claim_amount(&rewards_signer, 2 * DEFAULT_CLAIM_AMOUNT); + fund_vault(&rewards_signer, 1); + remove_admin(&rewards_signer, new_admin); + withdraw_from_vault(&rewards_signer, 2 * DEFAULT_CLAIM_AMOUNT); + + // Verify new state. + assert!(admins() == vector[@rewards]); + assert!(claim_amount() == 2 * DEFAULT_CLAIM_AMOUNT); + assert!(vault_balance() == 2 * DEFAULT_CLAIM_AMOUNT); + assert!( + coin::balance(@rewards) == 2 * DEFAULT_CLAIM_AMOUNT + ); + + // Verify that public key can be removed and re-added. + remove_public_keys( + &rewards_signer, + vector[claim_link_validated_public_key_bytes] + ); + assert!(!public_key_is_in_manifest(claim_link_validated_public_key_bytes)); + add_public_keys( + &rewards_signer, + vector[claim_link_validated_public_key_bytes] + ); + assert!(public_key_is_in_manifest(claim_link_validated_public_key_bytes)); + assert!(public_key_claimant(claim_link_validated_public_key_bytes) == NIL); + + // Get expected proceeds from swap. + let swap_event = + emojicoin_dot_fun::simulate_swap( + CLAIMANT, + @black_cat_market, + 2 * DEFAULT_CLAIM_AMOUNT, + false, + @integrator, + INTEGRATOR_FEE_RATE_BPS + ); + let (_, _, _, _, _, _, _, _, net_proceeds, _, _, _, _, _, _, _, _, _) = + emojicoin_dot_fun::unpack_swap(swap_event); + assert!(net_proceeds > 0); + + // Redeem a claim link. + redeem( + &get_signer(CLAIMANT), + ed25519::signature_to_bytes( + &ed25519::sign_arbitrary_bytes( + &claim_link_private_key, bcs::to_bytes(&CLAIMANT) + ) + ), + claim_link_validated_public_key_bytes, + @black_cat_market, + 1 + ); + + // Verify claimant's emojicoin balance. + assert!(coin::balance(CLAIMANT) == net_proceeds); + + // Check vault balance, manifest. + assert!(vault_balance() == 0); + assert!(public_key_claimant(claim_link_validated_public_key_bytes) == CLAIMANT); + + // Verify that public key entry can no longer be removed. + remove_public_keys( + &rewards_signer, + vector[claim_link_validated_public_key_bytes] + ); + assert!(public_key_is_in_manifest(claim_link_validated_public_key_bytes)); + + // Verify silent return for trying to remove public key not in manifest. + let (_, new_public_key) = ed25519::generate_keys(); + remove_public_keys( + &rewards_signer, + vector[ed25519::validated_public_key_to_bytes(&new_public_key)] + ); + } + + #[test, expected_failure(abort_code = E_INVALID_PUBLIC_KEY)] + fun test_public_key_claimant_invalid_public_key() acquires Vault { + let (_, claim_link_validated_public_key_bytes) = prepare_for_redemption(); + claim_link_validated_public_key_bytes.push_back(0); + public_key_claimant(claim_link_validated_public_key_bytes); + } + + #[test, expected_failure(abort_code = E_INVALID_PUBLIC_KEY)] + fun test_public_key_is_in_manifest_invalid_public_key() acquires Vault { + let (_, claim_link_validated_public_key_bytes) = prepare_for_redemption(); + claim_link_validated_public_key_bytes.push_back(0); + public_key_is_in_manifest(claim_link_validated_public_key_bytes); + } + + #[test, expected_failure(abort_code = E_CLAIM_LINK_ALREADY_CLAIMED)] + fun test_redeem_claim_link_already_claimed() acquires Vault { + let (signature_bytes, claim_link_validated_public_key_bytes) = + prepare_for_redemption(); + redeem( + &get_signer(CLAIMANT), + signature_bytes, + claim_link_validated_public_key_bytes, + @black_cat_market, + 1 + ); + redeem( + &get_signer(CLAIMANT), + signature_bytes, + claim_link_validated_public_key_bytes, + @black_cat_market, + 1 + ); + } + + #[test, expected_failure(abort_code = E_INVALID_CLAIM_LINK)] + fun test_redeem_invalid_claim_link() acquires Vault { + let (signature_bytes, claim_link_validated_public_key_bytes) = + prepare_for_redemption(); + remove_public_keys( + &get_signer(@rewards), + vector[claim_link_validated_public_key_bytes] + ); + redeem( + &get_signer(CLAIMANT), + signature_bytes, + claim_link_validated_public_key_bytes, + @black_cat_market, + 1 + ); + } + + #[test, expected_failure(abort_code = E_INVALID_PUBLIC_KEY)] + fun test_redeem_invalid_public_key() acquires Vault { + let (signature_bytes, claim_link_validated_public_key_bytes) = + prepare_for_redemption(); + claim_link_validated_public_key_bytes.push_back(0); + redeem( + &get_signer(CLAIMANT), + signature_bytes, + claim_link_validated_public_key_bytes, + @black_cat_market, + 1 + ); + } + + #[test, expected_failure(abort_code = E_INVALID_SIGNATURE)] + fun test_redeem_invalid_signature() acquires Vault { + let (signature_bytes, claim_link_validated_public_key_bytes) = + prepare_for_redemption(); + signature_bytes[0] = signature_bytes[0] ^ 0xff; + redeem( + &get_signer(CLAIMANT), + signature_bytes, + claim_link_validated_public_key_bytes, + @black_cat_market, + 1 + ); + } + + #[test, expected_failure(abort_code = E_VAULT_INSUFFICIENT_FUNDS)] + fun test_redeem_vault_insufficient_funds() acquires Vault { + let (signature_bytes, claim_link_validated_public_key_bytes) = + prepare_for_redemption(); + withdraw_from_vault(&get_signer(@rewards), DEFAULT_CLAIM_AMOUNT); + redeem( + &get_signer(CLAIMANT), + signature_bytes, + claim_link_validated_public_key_bytes, + @black_cat_market, + 1 + ); + } + + #[test, expected_failure(abort_code = E_NOT_ADMIN)] + fun test_remove_admin_not_admin() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + let not_admin = @0x2222; + assert!(not_admin != @rewards); + remove_admin(&get_signer(not_admin), @rewards); + } + + #[test, expected_failure(abort_code = E_ADMIN_TO_REMOVE_IS_NOT_ADMIN)] + fun test_remove_admin_admin_to_remove_is_not_admin() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + let not_admin = @0x2222; + assert!(not_admin != @rewards); + remove_admin(&rewards_signer, not_admin); + } + + #[test, expected_failure(abort_code = E_ADMIN_TO_REMOVE_IS_REWARDS_PUBLISHER)] + fun test_remove_admin_admin_to_remove_is_rewards_publisher() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + remove_admin(&rewards_signer, @rewards); + } + + #[test, expected_failure(abort_code = E_INVALID_PUBLIC_KEY)] + fun test_remove_public_keys_invalid_public_key() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + remove_public_keys(&rewards_signer, vector[vector[0x0]]); + } + + #[test, expected_failure(abort_code = E_NOT_ADMIN)] + fun test_remove_public_keys_not_admin() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + let not_admin_signer = get_signer(@0x2222); + assert!(&rewards_signer != ¬_admin_signer); + remove_public_keys(¬_admin_signer, vector[]); + } + + #[test, expected_failure(abort_code = E_NOT_ADMIN)] + fun test_set_claim_amount_not_admin() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + let not_admin_signer = get_signer(@0x2222); + assert!(¬_admin_signer != &rewards_signer); + set_claim_amount(¬_admin_signer, 1); + } + + #[test, expected_failure(abort_code = E_NOT_ADMIN)] + fun withdraw_from_vault_not_admin() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + let not_admin_signer = get_signer(@0x2222); + assert!(¬_admin_signer != &rewards_signer); + withdraw_from_vault(¬_admin_signer, 1); + } +} diff --git a/src/move/rewards/sources/emojicoin_dot_fun_rewards.move b/src/move/rewards/sources/emojicoin_dot_fun_rewards.move index b2cb660a1..4334c1e9e 100644 --- a/src/move/rewards/sources/emojicoin_dot_fun_rewards.move +++ b/src/move/rewards/sources/emojicoin_dot_fun_rewards.move @@ -44,41 +44,27 @@ module rewards::emojicoin_dot_fun_rewards { const E_APT_REWARD_AMOUNTS_DECREASING: u64 = 4; /// Expected number of rewards disbursed per tier per `APT_NOMINAL_VOLUME`. - const NOMINAL_N_REWARDS_PER_TIER: vector = vector[ - 1_500, - 500, - 200, - 50, - 5, - 1, - ]; + const NOMINAL_N_REWARDS_PER_TIER: vector = vector[1_500, 500, 200, 50, 5, 1]; /// Reward value in APT per reward per tier. - const APT_REWARD_AMOUNTS_PER_TIER: vector = vector[ - 1, - 2, - 5, - 10, - 100, - 500, - ]; + const APT_REWARD_AMOUNTS_PER_TIER: vector = vector[1, 2, 5, 10, 100, 500]; struct RewardTier has drop, store { apt_amount_per_reward: u64, n_rewards_disbursed: u64, n_rewards_remaining: u64, - reward_probability_per_octa_of_swap_fees_paid_q64: u128, + reward_probability_per_octa_of_swap_fees_paid_q64: u128 } struct Vault has key { signer_capability: SignerCapability, - reward_tiers: vector, + reward_tiers: vector } #[event] struct EmojicoinDotFunRewards has copy, drop, store { swap: Swap, - octas_reward_amount: u64, + octas_reward_amount: u64 } #[randomness] @@ -87,22 +73,22 @@ module rewards::emojicoin_dot_fun_rewards { market_address: address, input_amount: u64, is_sell: bool, - min_output_amount: u64, + min_output_amount: u64 ) acquires Vault { // Simulate swap to get integrator fee, then execute swap. let swapper_address = signer::address_of(swapper); - let swap = emojicoin_dot_fun::simulate_swap( - swapper_address, - market_address, - input_amount, - is_sell, - @integrator, - INTEGRATOR_FEE_RATE_BPS, - ); - let ( - _, _, _, _, _, _, _, _, _, _, _, _, integrator_fee_in_octas, _, _, _, _, _, - ) = emojicoin_dot_fun::unpack_swap(swap); + let swap = + emojicoin_dot_fun::simulate_swap( + swapper_address, + market_address, + input_amount, + is_sell, + @integrator, + INTEGRATOR_FEE_RATE_BPS + ); + let (_, _, _, _, _, _, _, _, _, _, _, _, integrator_fee_in_octas, _, _, _, _, _) = + emojicoin_dot_fun::unpack_swap(swap); emojicoin_dot_fun::swap( swapper, market_address, @@ -110,7 +96,7 @@ module rewards::emojicoin_dot_fun_rewards { is_sell, @integrator, INTEGRATOR_FEE_RATE_BPS, - min_output_amount, + min_output_amount ); // Get vault balance, returning early if empty. @@ -138,7 +124,8 @@ module rewards::emojicoin_dot_fun_rewards { // `probability_per_octa_q64` is less than 1 in nominal probability, hence less than // `MAX_U64` when represented as a Q64-style `u128`. Since `integrator_fee_in_octas` is // also significantly less than `MAX_U64`, the product is less than `MAX_U128`. - let reward_threshold_q64 = probability_per_octa_q64 * (integrator_fee_in_octas as u128); + let reward_threshold_q64 = + probability_per_octa_q64 * (integrator_fee_in_octas as u128); // Check if user is eligible for reward at current tier by checking if tier still has // rewards remaining, then compare random number to probability threshold. Since the @@ -154,37 +141,46 @@ module rewards::emojicoin_dot_fun_rewards { // disbursement effectively requires an additional GOTO statement (`BrFalse`) in bytecode // for the false branch. if (!option::is_none(&highest_winning_tier_index)) { - let highest_winning_tier_index = option::destroy_some(highest_winning_tier_index); - let tier_ref_mut = vector::borrow_mut(tiers_ref_mut, highest_winning_tier_index); + let highest_winning_tier_index = + option::destroy_some(highest_winning_tier_index); + let tier_ref_mut = vector::borrow_mut( + tiers_ref_mut, highest_winning_tier_index + ); tier_ref_mut.n_rewards_remaining = tier_ref_mut.n_rewards_remaining - 1; tier_ref_mut.n_rewards_disbursed = tier_ref_mut.n_rewards_disbursed + 1; - let octas_reward_amount = tier_ref_mut.apt_amount_per_reward * OCTAS_PER_APT; - let vault_signer = account::create_signer_with_capability(vault_signer_cap_ref); - event::emit(EmojicoinDotFunRewards{ swap, octas_reward_amount }); + let octas_reward_amount = tier_ref_mut.apt_amount_per_reward + * OCTAS_PER_APT; + let vault_signer = + account::create_signer_with_capability(vault_signer_cap_ref); + event::emit(EmojicoinDotFunRewards { swap, octas_reward_amount }); aptos_account::transfer(&vault_signer, swapper_address, octas_reward_amount); } } /// To fund 10 rewards in the first tier and 5 rewards in the second tier, pass /// `n_rewards_to_fund_per_tier` as `vector[10, 5, ...]`. - public entry fun fund_tiers(funder: &signer, n_rewards_to_fund_per_tier: vector) - acquires Vault - { + public entry fun fund_tiers( + funder: &signer, n_rewards_to_fund_per_tier: vector + ) acquires Vault { // Check tiers. let vault_ref_mut = borrow_global_mut(@rewards); let n_tiers = vector::length(&NOMINAL_N_REWARDS_PER_TIER); let tiers_ref_mut = &mut vault_ref_mut.reward_tiers; - assert!(vector::length(&n_rewards_to_fund_per_tier) == n_tiers, E_N_TIERS_MISMATCH); + assert!( + vector::length(&n_rewards_to_fund_per_tier) == n_tiers, E_N_TIERS_MISMATCH + ); // Calculate total amount to fund, updating remaining rewards for each tier. let octas_to_fund = 0; - for (i in 0..n_tiers) { + for (i in 0..n_tiers) { let tier_ref_mut = vector::borrow_mut(tiers_ref_mut, i); - let n_rewards_to_fund_this_tier = *vector::borrow(&n_rewards_to_fund_per_tier, i); - octas_to_fund = octas_to_fund + - n_rewards_to_fund_this_tier * tier_ref_mut.apt_amount_per_reward * OCTAS_PER_APT; - tier_ref_mut.n_rewards_remaining = - tier_ref_mut.n_rewards_remaining + n_rewards_to_fund_this_tier; + let n_rewards_to_fund_this_tier = + *vector::borrow(&n_rewards_to_fund_per_tier, i); + octas_to_fund = octas_to_fund + + n_rewards_to_fund_this_tier * tier_ref_mut.apt_amount_per_reward + * OCTAS_PER_APT; + tier_ref_mut.n_rewards_remaining = tier_ref_mut.n_rewards_remaining + + n_rewards_to_fund_this_tier; }; // Transfer rewards to vault. @@ -194,35 +190,43 @@ module rewards::emojicoin_dot_fun_rewards { } fun init_module(rewards: &signer) { - let (vault_signer, signer_capability) = account::create_resource_account(rewards, VAULT); - move_to(rewards, Vault { - signer_capability, - reward_tiers: reward_tiers(), - }); + let (vault_signer, signer_capability) = + account::create_resource_account(rewards, VAULT); + move_to( + rewards, + Vault { signer_capability, reward_tiers: reward_tiers() } + ); coin::register(&vault_signer); } fun reward_tiers(): vector { // Check tier count. let n_tiers = vector::length(&NOMINAL_N_REWARDS_PER_TIER); - assert!(vector::length(&APT_REWARD_AMOUNTS_PER_TIER) == n_tiers, E_N_TIERS_MISMATCH); + assert!( + vector::length(&APT_REWARD_AMOUNTS_PER_TIER) == n_tiers, E_N_TIERS_MISMATCH + ); // Check number of rewards, and reward amount in APT: total, and for each tier. let n_rewards_total = 0; let apt_total_reward_amount = 0; let n_rewards_per_tier_last = *vector::borrow(&NOMINAL_N_REWARDS_PER_TIER, 0); - let apt_reward_amount_last_tier = *vector::borrow(&APT_REWARD_AMOUNTS_PER_TIER, 0); - for (i in 0..n_tiers) { + let apt_reward_amount_last_tier = *vector::borrow( + &APT_REWARD_AMOUNTS_PER_TIER, 0 + ); + for (i in 0..n_tiers) { let n_rewards_this_tier = *vector::borrow(&NOMINAL_N_REWARDS_PER_TIER, i); - assert!(n_rewards_this_tier <= n_rewards_per_tier_last, E_N_REWARDS_INCREASING); + assert!( + n_rewards_this_tier <= n_rewards_per_tier_last, E_N_REWARDS_INCREASING + ); n_rewards_total = n_rewards_total + n_rewards_this_tier; - let apt_reward_amount_this_tier = *vector::borrow(&APT_REWARD_AMOUNTS_PER_TIER, i); + let apt_reward_amount_this_tier = + *vector::borrow(&APT_REWARD_AMOUNTS_PER_TIER, i); assert!( apt_reward_amount_this_tier >= apt_reward_amount_last_tier, E_APT_REWARD_AMOUNTS_DECREASING ); - apt_total_reward_amount = apt_total_reward_amount + - apt_reward_amount_this_tier * n_rewards_this_tier; + apt_total_reward_amount = apt_total_reward_amount + + apt_reward_amount_this_tier * n_rewards_this_tier; n_rewards_per_tier_last = n_rewards_this_tier; apt_reward_amount_last_tier = apt_reward_amount_this_tier; }; @@ -230,35 +234,42 @@ module rewards::emojicoin_dot_fun_rewards { // Check expected value of rewards against nominal fees paid. let octas_nominal_volume = APT_NOMINAL_VOLUME * OCTAS_PER_APT; - let octas_nominal_fees = ( + let octas_nominal_fees = ( - (octas_nominal_volume as u128) * (INTEGRATOR_FEE_RATE_BPS as u128) / - (BASIS_POINTS_PER_UNIT as u128) - ) as u64 - ); + ((octas_nominal_volume as u128) * (INTEGRATOR_FEE_RATE_BPS as u128) + / (BASIS_POINTS_PER_UNIT as u128)) as u64 + ); let octas_total_reward_amount = apt_total_reward_amount * OCTAS_PER_APT; assert!(octas_total_reward_amount <= octas_nominal_fees, E_EXPECTED_VALUE); // Construct tiers. let reward_tiers = vector[]; for (i in 0..n_tiers) { - vector::push_back(&mut reward_tiers, RewardTier { - apt_amount_per_reward: *vector::borrow(&APT_REWARD_AMOUNTS_PER_TIER, i), - n_rewards_disbursed: 0, - n_rewards_remaining: 0, - // Once `octas_nominal_fees` have been paid, `NOMINAL_N_REWARDS_PER_TIER[i]` rewards - // will have been disbursed, so the probability of winning a reward for an octa of - // fees paid is the ratio of the number of rewards in this tier to the total number - // of nominal fees paid after all rewards for this tier have been disbursed. - reward_probability_per_octa_of_swap_fees_paid_q64: - ( - ((*vector::borrow(&NOMINAL_N_REWARDS_PER_TIER, i) as u128) << SHIFT_Q64) / - (octas_nominal_fees as u128) + vector::push_back( + &mut reward_tiers, + RewardTier { + apt_amount_per_reward: *vector::borrow( + &APT_REWARD_AMOUNTS_PER_TIER, i + ), + n_rewards_disbursed: 0, + n_rewards_remaining: 0, + // Once `octas_nominal_fees` have been paid, `NOMINAL_N_REWARDS_PER_TIER[i]` + // rewards will have been disbursed, so the probability of winning a reward for + // an octa of fees paid is the ratio of the number of rewards in this tier to + // the total number of nominal fees paid after all rewards for this tier have + // been disbursed. + reward_probability_per_octa_of_swap_fees_paid_q64: ( + ((*vector::borrow(&NOMINAL_N_REWARDS_PER_TIER, i) as u128) + << SHIFT_Q64) / (octas_nominal_fees as u128) ) - }); + } + ); }; reward_tiers } - #[test] fun test_reward_tiers() { reward_tiers(); } + #[test] + fun test_reward_tiers() { + reward_tiers(); + } } From e6cd6b1278ff8ff148599b086f60a121e3097a36 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Fri, 15 Nov 2024 04:14:14 +0100 Subject: [PATCH 37/94] [ECO-2396] Optimize candlesticks (#348) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- .../frontend/src/app/candlesticks/route.ts | 215 +++++++++++++----- .../frontend/src/app/candlesticks/utils.ts | 86 +++++++ .../src/components/charts/PrivateChart.tsx | 26 ++- src/typescript/sdk/src/const.ts | 4 + .../indexer-v2/queries/app/candlesticks.ts | 65 ------ .../sdk/src/indexer-v2/queries/app/index.ts | 1 - .../sdk/src/indexer-v2/queries/app/market.ts | 26 ++- .../sdk/src/indexer-v2/queries/utils.ts | 37 ++- .../sdk/src/indexer-v2/types/common.ts | 3 +- 9 files changed, 318 insertions(+), 145 deletions(-) create mode 100644 src/typescript/frontend/src/app/candlesticks/utils.ts delete mode 100644 src/typescript/sdk/src/indexer-v2/queries/app/candlesticks.ts diff --git a/src/typescript/frontend/src/app/candlesticks/route.ts b/src/typescript/frontend/src/app/candlesticks/route.ts index 187961515..dfcd22274 100644 --- a/src/typescript/frontend/src/app/candlesticks/route.ts +++ b/src/typescript/frontend/src/app/candlesticks/route.ts @@ -1,80 +1,191 @@ -import { fetchPeriodicEventsSince } from "@/queries/market"; -import { type Period, toPeriod } from "@sdk/index"; -import { type PeriodicStateEventModel } from "@sdk/indexer-v2/types"; -import { type PeriodTypeFromDatabase } from "@sdk/indexer-v2/types/json-types"; +// cspell:word timespan + +import { type AnyNumberString, getPeriodStartTimeFromTime, toPeriod } from "@sdk/index"; import { parseInt } from "lodash"; -import { unstable_cache } from "next/cache"; import { type NextRequest } from "next/server"; -import { stringifyJSON } from "utils"; +import { + type CandlesticksSearchParams, + type GetCandlesticksParams, + getPeriodDurationSeconds, + HISTORICAL_CACHE_DURATION, + indexToParcelEndDate, + indexToParcelStartDate, + isValidCandlesticksSearchParams, + jsonStrAppend, + NORMAL_CACHE_DURATION, + PARCEL_SIZE, + toIndex, +} from "./utils"; +import { unstable_cache } from "next/cache"; +import { getLatestProcessedEmojicoinTimestamp } from "@sdk/indexer-v2/queries/utils"; +import { parseJSON, stringifyJSON } from "utils"; +import { fetchMarketRegistration, fetchPeriodicEventsSince } from "@/queries/market"; + +/** + * @property `data` the stringified version of {@link CandlesticksDataType}. + * @property `count` the number of rows returned. + */ +type GetCandlesticksResponse = { + data: string; + count: number; +}; + +type CandlesticksDataType = Awaited>; + +const getCandlesticks = async (params: GetCandlesticksParams) => { + const { marketID, index, period } = params; -const CANDLESTICKS_LIMIT = 500; + const start = indexToParcelStartDate(index, period); -type QueryParams = { - marketID: number; - start: Date; - period: Period; - limit: number; + const periodDurationMilliseconds = getPeriodDurationSeconds(period) * 1000; + const timespan = periodDurationMilliseconds * PARCEL_SIZE; + const end = new Date(start.getTime() + timespan); + + // PARCEL_SIZE determines the max number of rows, so we don't need to pass a `LIMIT` value. + // `start` and `end` determine the level of pagination, so no need to specify `offset` either. + const data = await fetchPeriodicEventsSince({ + marketID, + period, + start, + end, + }); + + return { + data: stringifyJSON(data), + count: data.length, + }; }; -const getCandlesticks = async (params: QueryParams) => { - const { marketID, start, period, limit } = params; - const aggregate: PeriodicStateEventModel[] = []; - - while (aggregate.length < limit) { - const data = await fetchPeriodicEventsSince({ - marketID, - period, - start, - offset: aggregate.length, - limit: limit - aggregate.length, - }); - aggregate.push(...data); - if (data.length < limit) { - break; +/** + * Returns the market registration event for a market if it exists. + * + * If it doesn't exist, it throws an error so that the value isn't cached in the + * `unstable_cache` call. + * + * @see {@link getCachedMarketRegistrationMs} + */ +const getMarketRegistrationMs = async (marketID: AnyNumberString) => + fetchMarketRegistration({ marketID }).then((res) => { + if (res) { + return Number(res.market.time / 1000n); } + throw new Error("Market is not yet registered."); + }); + +const getCachedMarketRegistrationMs = unstable_cache( + getMarketRegistrationMs, + ["market-registrations"], + { + revalidate: HISTORICAL_CACHE_DURATION, } +); - return stringifyJSON(aggregate); -}; +/** + * Fetch all of the parcels of candlesticks that have completely ended. + * The only difference between this and {@link getNormalCachedCandlesticks} is the cache tag and + * thus how long the data is cached for. + */ +const getHistoricCachedCandlesticks = unstable_cache(getCandlesticks, ["candlesticks-historic"], { + revalidate: HISTORICAL_CACHE_DURATION, +}); + +/** + * Fetch all candlestick parcels that haven't completed yet. + * The only difference between this and {@link getHistoricCachedCandlesticks} is the cache tag and + * thus how long the data is cached for. + */ +const getNormalCachedCandlesticks = unstable_cache(getCandlesticks, ["candlesticks"], { + revalidate: NORMAL_CACHE_DURATION, +}); -const getCachedCandlesticks = unstable_cache(getCandlesticks, ["candlesticks"], { revalidate: 10 }); +const getCachedLatestProcessedEmojicoinTimestamp = unstable_cache( + getLatestProcessedEmojicoinTimestamp, + ["processor-timestamp"], + { revalidate: 5 } +); /* eslint-disable-next-line import/no-unused-modules */ export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; - const marketIDStr = searchParams.get("marketID"); - const startStr = searchParams.get("start"); - const periodStr = searchParams.get("period"); - const limitStr = searchParams.get("limit"); + const params: CandlesticksSearchParams = { + marketID: searchParams.get("marketID"), + to: searchParams.get("to"), + period: searchParams.get("period"), + countBack: searchParams.get("countBack"), + }; - if (!marketIDStr || !startStr || !periodStr || !limitStr) { - return new Response("", { status: 400 }); + if (!isValidCandlesticksSearchParams(params)) { + return new Response("Invalid candlestick search params.", { status: 400 }); } - if (isNaN(parseInt(marketIDStr))) { - return new Response("", { status: 400 }); - } + const marketID = parseInt(params.marketID); + const to = parseInt(params.to); + const period = toPeriod(params.period); + const countBack = parseInt(params.countBack); + const numParcels = parseInt(params.amount); + + const index = toIndex(to, period); - if (isNaN(parseInt(startStr))) { - return new Response("", { status: 400 }); + // Ensure that the last start date as calculated per the search params is valid. + // This is specifically the last parcel's start date- aka the last parcel's first candlestick's + // start time. + const lastParcelStartDate = indexToParcelStartDate(index + numParcels - 1, period); + if (lastParcelStartDate > new Date()) { + return new Response("The last parcel's start date cannot be later than the current time.", { + status: 400, + }); } - let period: Period; + let data: string = "[]"; + + const processorTimestamp = new Date(await getCachedLatestProcessedEmojicoinTimestamp()); + + let totalCount = 0; + let i = 0; + + let registrationPeriodBoundaryStart: Date; try { - period = toPeriod(periodStr as PeriodTypeFromDatabase); + registrationPeriodBoundaryStart = await getCachedMarketRegistrationMs(marketID).then( + (time) => new Date(Number(getPeriodStartTimeFromTime(time, period))) + ); } catch { - return new Response("", { status: 400 }); + return new Response("Market has not been registered yet.", { status: 400 }); } - if (isNaN(parseInt(limitStr)) || parseInt(limitStr) > CANDLESTICKS_LIMIT) { - return new Response("", { status: 400 }); - } - - const start = new Date(parseInt(startStr) * 1000); - const limit = parseInt(limitStr); - const marketID = parseInt(marketIDStr); + while (totalCount <= countBack) { + const localIndex = index - i; + const endDate = indexToParcelEndDate(localIndex, period); + let res: GetCandlesticksResponse; + if (endDate < processorTimestamp) { + res = await getHistoricCachedCandlesticks({ + marketID, + index: localIndex, + period, + }); + } else { + res = await getNormalCachedCandlesticks({ + marketID, + index: localIndex, + period, + }); + } - const data = await getCachedCandlesticks({ marketID, start, period, limit }); + if (i == 0) { + const parsed = parseJSON(res.data); + const filtered = parsed.filter( + (val) => val.periodicMetadata.startTime < BigInt(to) * 1_000_000n + ); + totalCount += filtered.length; + data = jsonStrAppend(data, stringifyJSON(filtered)); + } else { + totalCount += res.count; + data = jsonStrAppend(data, res.data); + } + if (endDate < registrationPeriodBoundaryStart) { + break; + } + i++; + } return new Response(data); } diff --git a/src/typescript/frontend/src/app/candlesticks/utils.ts b/src/typescript/frontend/src/app/candlesticks/utils.ts new file mode 100644 index 000000000..82e424e5e --- /dev/null +++ b/src/typescript/frontend/src/app/candlesticks/utils.ts @@ -0,0 +1,86 @@ +import { isPeriod, type Period, PeriodDuration, periodEnumToRawDuration } from "@sdk/index"; + +/** + * Parcel size is the amount of candlestick periods that will be in a single parcel. + * That is, a parcel for 1m candlesticks will be `PARCEL_SIZE` minutes of time. + * + * Note that this is *NOT* the number of candlesticks in the database- as there may be gaps in the + * on-chain data (and thus the database). + * + * More specifically, each parcel will have anywhere from 0 to PARCEL_SIZE number of candlesticks + * and will always span `PARCEL_SIZE` candlesticks/periods worth of time. + */ +export const PARCEL_SIZE = 500; + +export const indexToParcelStartDate = (index: number, period: Period): Date => + new Date((PARCEL_SIZE * (index * periodEnumToRawDuration(period))) / 1000); +export const indexToParcelEndDate = (index: number, period: Period): Date => + new Date((PARCEL_SIZE * ((index + 1) * periodEnumToRawDuration(period))) / 1000); + +export const getPeriodDurationSeconds = (period: Period) => + (periodEnumToRawDuration(period) / PeriodDuration.PERIOD_1M) * 60; + +export const toIndex = (end: number, period: Period): number => { + const periodDuration = getPeriodDurationSeconds(period); + const parcelDuration = periodDuration * PARCEL_SIZE; + + const index = Math.floor(end / parcelDuration); + + return index; +}; + +export const jsonStrAppend = (a: string, b: string): string => { + if (a === "[]") return b; + if (b === "[]") return a; + return `${a.substring(0, a.length - 1)},${b.substring(1)}`; +}; + +export type GetCandlesticksParams = { + marketID: number; + index: number; + period: Period; +}; + +/** + * The search params used in the `GET` request at `candlesticks/api`. + * + * @property {string} marketID - The market ID. + * @property {string} to - The end time boundary. + * @property {string} period - The {@link Period}. + * @property {string} countBack - The `countBack` value requested by the datafeed API. + */ +export type CandlesticksSearchParams = { + marketID: string | null; + to: string | null; + period: string | null; + countBack: string | null; +}; + +/** + * Validated {@link CandlesticksSearchParams}. + */ +export type ValidCandlesticksSearchParams = { + marketID: string; + to: string; + period: Period; + amount: string; + countBack: string; +}; + +const isNumber = (s: string) => !isNaN(parseInt(s)); + +export const isValidCandlesticksSearchParams = ( + params: CandlesticksSearchParams +): params is ValidCandlesticksSearchParams => { + const { marketID, to, period, countBack } = params; + // prettier-ignore + return ( + marketID !== null && isNumber(marketID) && + to !== null && isNumber(to) && + countBack !== null && isNumber(countBack) && + period !== null && isPeriod(period) + ); +}; + +export const HISTORICAL_CACHE_DURATION = 60 * 60 * 24 * 365; // 1 year. +export const NORMAL_CACHE_DURATION = 10; // 10 seconds. diff --git a/src/typescript/frontend/src/components/charts/PrivateChart.tsx b/src/typescript/frontend/src/components/charts/PrivateChart.tsx index bd6d3a1ae..dc0cb2552 100644 --- a/src/typescript/frontend/src/components/charts/PrivateChart.tsx +++ b/src/typescript/frontend/src/components/charts/PrivateChart.tsx @@ -33,7 +33,7 @@ import { getSymbolEmojisInString, symbolToEmojis, toMarketEmojiData } from "@sdk import { type PeriodicStateEventModel, type MarketMetadataModel } from "@sdk/indexer-v2/types"; import { getMarketResource } from "@sdk/markets"; import { Aptos } from "@aptos-labs/ts-sdk"; -import { PeriodDuration, periodEnumToRawDuration, Trigger } from "@sdk/const"; +import { periodEnumToRawDuration, Trigger } from "@sdk/const"; import { type LatestBar, marketToLatestBars, @@ -153,26 +153,28 @@ export const Chart = (props: ChartContainerProps) => { onHistoryCallback, onErrorCallback ) => { - const { from, to } = periodParams; + const { to, countBack } = periodParams; + try { const period = ResolutionStringToPeriod[resolution.toString()]; const periodDuration = periodEnumToRawDuration(period); - const periodDurationSeconds = (periodDuration / PeriodDuration.PERIOD_1M) * 60; - const end = new Date(to * 1000); + // The start timestamp is rounded so that all the people who load the webpage at a similar time get served // the same cached response. - const start = from - (from % periodDurationSeconds); const params = new URLSearchParams({ marketID: props.marketID, - start: start.toString(), period: period.toString(), - limit: "500", + countBack: countBack.toString(), + to: to.toString(), }); const data: PeriodicStateEventModel[] = await fetch(`/candlesticks?${params.toString()}`) .then((res) => res.text()) .then((res) => parseJSON(res)); - const isFetchForMostRecentBars = end.getTime() - new Date().getTime() > 1000; + data.sort((a, b) => Number(a.periodicMetadata.startTime - b.periodicMetadata.startTime)); + + const endDate = new Date(to * 1000); + const isFetchForMostRecentBars = endDate.getTime() - new Date().getTime() > 1000; // If the end time is in the future, it means that `getBars` is being called for the most recent candlesticks, // and thus we should append the latest candlestick to this dataset to ensure the chart is up to date. @@ -213,6 +215,7 @@ export const Chart = (props: ChartContainerProps) => { const nonce = marketResource.sequenceInfo.nonce; latestBar = periodicStateTrackerToLatestBar(tracker, nonce); } + // Filter the data so that all resulting bars are within the specified time range. // Also, update the `open` price to the previous bar's `close` price if it exists. // NOTE: Since `getBars` is called multiple times, this will result in several @@ -220,7 +223,9 @@ export const Chart = (props: ChartContainerProps) => { // some visual inconsistencies in the chart. const bars: Bar[] = data.reduce((acc: Bar[], event) => { const bar = toBar(event); - const inTimeRange = bar.time >= from * 1000 && bar.time <= to * 1000; + // Only exclude bars that are after `to`. + // see: https://www.tradingview.com/charting-library-docs/latest/connecting_data/datafeed-api/required-methods#getbars + const inTimeRange = bar.time <= to * 1000; if (inTimeRange && hasTradingActivity(bar)) { const prev = acc.at(-1); if (prev) { @@ -275,9 +280,8 @@ export const Chart = (props: ChartContainerProps) => { return; } } - onHistoryCallback(bars, { - noData: bars.length === 0, + noData: bars.length === 0, // && notAllEmptyBars, }); } catch (e) { if (e instanceof Error) { diff --git a/src/typescript/sdk/src/const.ts b/src/typescript/sdk/src/const.ts index f814dccfc..d936c75ac 100644 --- a/src/typescript/sdk/src/const.ts +++ b/src/typescript/sdk/src/const.ts @@ -238,3 +238,7 @@ export const PERIODS = [ Period.Period4H, Period.Period1D, ]; + +const PERIODS_STRINGS_SET = new Set(PERIODS.map(String)); + +export const isPeriod = (period: string): period is Period => PERIODS_STRINGS_SET.has(period); diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/candlesticks.ts b/src/typescript/sdk/src/indexer-v2/queries/app/candlesticks.ts deleted file mode 100644 index 24bc6d499..000000000 --- a/src/typescript/sdk/src/indexer-v2/queries/app/candlesticks.ts +++ /dev/null @@ -1,65 +0,0 @@ -"use server"; - -import { type Period } from "../../../const"; -import { LIMIT } from "../../../queries/const"; -import { type PeriodicStateEventModel } from "../../types"; -import { fetchPeriodicEventsSince } from "./market"; - -/** - * Query to exhaustively fetch all candlesticks (periodic state events) in a given time range. - * Since this function's data will be used for frontend charting, it's - * written with non-blocking pseudo-recursive setTimeout calls to avoid - * blocking the main javascript execution thread. - * - * @param marketID The market ID to fetch data for. - * @param start The start time of the range to fetch data from. - * @param end The end time of the range to fetch data from. - * @param period The period of the candlesticks to fetch. - * @param limit Optional limit to the number of elements to fetch per request. Defaults to `LIMIT`. - * @param fetchDelay Optional delay in milliseconds between each fetch request. - * @returns an Array. - */ -/* eslint-disable-next-line import/no-unused-modules */ -export async function fetchAllCandlesticksInTimeRange(args: { - marketID: string; - start: Date; - end: Date; - period: Period; - limit?: number; - fetchDelay?: number; -}): Promise { - const { marketID, start, end, period, limit = LIMIT, fetchDelay = 0 } = args; - const aggregate: PeriodicStateEventModel[] = []; - let keepFetching = true; - - /* eslint-disable consistent-return */ - const fetchData = async ( - resolve: (value: PeriodicStateEventModel[] | PromiseLike) => void, - reject: (reason?: string | Error) => void - ) => { - /* eslint-enable consistent-return */ - if (!keepFetching) { - return resolve(aggregate); - } - try { - const data = await fetchPeriodicEventsSince({ - marketID, - period, - start, - offset: aggregate.length, - limit, - }); - const filtered = data.filter((d) => d.transaction.timestamp.getTime() <= end.getTime()); - keepFetching = filtered.length === limit; - aggregate.push(...filtered); - - setTimeout(() => fetchData(resolve, reject), fetchDelay); - } catch (err) { - return reject(err as string | Error); - } - }; - - return new Promise((resolve, reject) => { - fetchData(resolve, reject); - }); -} diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/index.ts b/src/typescript/sdk/src/indexer-v2/queries/app/index.ts index 0009eab6e..e3dc8266e 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/app/index.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/app/index.ts @@ -1,4 +1,3 @@ export * from "./home"; export * from "./market"; export * from "./pools"; -export * from "./candlesticks"; diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/market.ts b/src/typescript/sdk/src/indexer-v2/queries/app/market.ts index bf079dfe2..7060e8634 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/app/market.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/app/market.ts @@ -9,6 +9,7 @@ import { postgrest, toQueryArray } from "../client"; import { queryHelper, queryHelperSingle } from "../utils"; import { toChatEventModel, + toMarketRegistrationEventModel, toMarketStateModel, toPeriodicStateEventModel, toSwapEventModel, @@ -46,17 +47,18 @@ const selectPeriodicEventsSince = ({ marketID, period, start, - offset, - limit = LIMIT, -}: PeriodicStateEventQueryArgs) => - postgrest + end, +}: PeriodicStateEventQueryArgs) => { + const query = postgrest .from(TableName.PeriodicStateEvents) .select("*") .eq("market_id", marketID) .eq("period", period) .gte("start_time", start.toISOString()) - .order("start_time", ORDER_BY.ASC) - .range(offset, offset + limit - 1); + .lt("start_time", end.toISOString()) + .order("start_time", ORDER_BY.ASC); + return query; +}; const selectMarketState = ({ searchEmojis }: { searchEmojis: SymbolEmoji[] }) => postgrest @@ -66,6 +68,14 @@ const selectMarketState = ({ searchEmojis }: { searchEmojis: SymbolEmoji[] }) => .limit(1) .single(); +const selectMarketRegistration = ({ marketID }: { marketID: AnyNumberString }) => + postgrest + .from(TableName.MarketRegistrationEvents) + .select("*") + .eq("market_id", marketID) + .limit(1) + .single(); + export const fetchSwapEvents = queryHelper(selectSwapsByMarketID, toSwapEventModel); export const fetchChatEvents = queryHelper(selectChatsByMarketID, toChatEventModel); export const fetchPeriodicEventsSince = queryHelper( @@ -73,3 +83,7 @@ export const fetchPeriodicEventsSince = queryHelper( toPeriodicStateEventModel ); export const fetchMarketState = queryHelperSingle(selectMarketState, toMarketStateModel); +export const fetchMarketRegistration = queryHelperSingle( + selectMarketRegistration, + toMarketRegistrationEventModel +); diff --git a/src/typescript/sdk/src/indexer-v2/queries/utils.ts b/src/typescript/sdk/src/indexer-v2/queries/utils.ts index d0311d9f8..e9200c6ba 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/utils.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/utils.ts @@ -11,7 +11,7 @@ import { } from "@supabase/postgrest-js"; import { type Account, type AccountAddressInput } from "@aptos-labs/ts-sdk"; import { type AnyNumberString } from "../../types/types"; -import { type DatabaseJsonType, TableName } from "../types/json-types"; +import { type DatabaseJsonType, postgresTimestampToDate, TableName } from "../types/json-types"; import { toAccountAddress } from "../../utils"; import { postgrest } from "./client"; import { type DatabaseModels } from "../types"; @@ -42,23 +42,44 @@ const extractRows = (res: PostgrestSingleResponse>) => res.data ?? ( // NOTE: If we ever add another processor type to the indexer processor stack, this will need to be // updated, because it is assumed here that there is a single row returned. Multiple processors // would mean there would be multiple rows. -export const getLatestProcessedEmojicoinVersion = async () => +export const getProcessorStatus = async () => postgrest .from(TableName.ProcessorStatus) - .select("last_success_version") + .select("processor, last_success_version, last_updated, last_transaction_timestamp") .limit(1) .single() .then((r) => { - const rowWithVersion = extractRow(r); - if (!rowWithVersion) { + const row = extractRow(r); + if (!row) { console.error(r); throw new Error("No processor status row found."); } - if (!("last_success_version" in rowWithVersion)) { - console.warn("Couldn't find `last_success_version` in the response data.", r); + if ( + !( + "processor" in row && + "last_success_version" in row && + "last_updated" in row && + "last_transaction_timestamp" in row + ) + ) { + console.warn("Couldn't find all fields in the row response data.", r); } - return BigInt(rowWithVersion.last_success_version); + return { + processor: row.processor, + lastSuccessVersion: BigInt(row.last_success_version), + lastUpdated: postgresTimestampToDate(row.last_updated), + lastTransactionTimestamp: row.last_transaction_timestamp + ? postgresTimestampToDate(row.last_transaction_timestamp) + : new Date(0), // Provide a default, because this field is nullable. + }; }); + +export const getLatestProcessedEmojicoinVersion = async () => + getProcessorStatus().then((r) => r.lastSuccessVersion); + +export const getLatestProcessedEmojicoinTimestamp = async () => + getProcessorStatus().then((r) => r.lastTransactionTimestamp); + /** * Wait for the processed version of a table or view to be at least the given version. */ diff --git a/src/typescript/sdk/src/indexer-v2/types/common.ts b/src/typescript/sdk/src/indexer-v2/types/common.ts index 354987aad..25d75c549 100644 --- a/src/typescript/sdk/src/indexer-v2/types/common.ts +++ b/src/typescript/sdk/src/indexer-v2/types/common.ts @@ -25,7 +25,6 @@ export type MarketStateQueryArgs = { export type PeriodicStateEventQueryArgs = { marketID: AnyNumberString; start: Date; - offset: number; + end: Date; period: Period; - limit?: number; } & Omit; From 8d5660f5045974095c4103dee8bb6adddeaa9cab Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Fri, 15 Nov 2024 05:21:06 +0100 Subject: [PATCH 38/94] [ECO-2388] Resolve promises concurrently (#349) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- src/typescript/frontend/src/app/home/page.tsx | 35 ++++++++++++++----- .../frontend/src/app/market/[market]/page.tsx | 10 ++++-- .../components/main-info/BondingProgress.tsx | 2 +- .../animation-variants/event-variants.ts | 2 +- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/typescript/frontend/src/app/home/page.tsx b/src/typescript/frontend/src/app/home/page.tsx index 47647c595..f59510576 100644 --- a/src/typescript/frontend/src/app/home/page.tsx +++ b/src/typescript/frontend/src/app/home/page.tsx @@ -9,6 +9,8 @@ import { } from "@/queries/home"; import { symbolBytesToEmojis } from "@sdk/emoji_data"; import { MARKETS_PER_PAGE } from "lib/queries/sorting/const"; +import { ORDER_BY } from "@sdk/queries"; +import { SortMarketsBy } from "@sdk/indexer-v2/types/common"; export const revalidate = 2; @@ -16,12 +18,14 @@ export default async function Home({ searchParams }: HomePageParams) { const { page, sortBy, orderBy, q } = toHomePageParamsWithDefault(searchParams); const searchEmojis = q ? symbolBytesToEmojis(q).emojis.map((e) => e.emoji) : undefined; - const featured = await fetchFeaturedMarket(); - let numMarkets: number; - let markets: Awaited>["rows"]; + const priceFeedPromise = fetchPriceFeed({}); + + let marketsPromise: ReturnType; + + let numMarketsPromise: Promise; if (searchEmojis?.length) { - const res = await fetchMarketsWithCount({ + const promise = fetchMarketsWithCount({ page, sortBy, orderBy, @@ -29,20 +33,33 @@ export default async function Home({ searchParams }: HomePageParams) { pageSize: MARKETS_PER_PAGE, count: true, }); - numMarkets = res.count!; - markets = res.rows; + marketsPromise = promise.then((r) => r.rows); + numMarketsPromise = promise.then((r) => r.count!); } else { - numMarkets = await fetchNumRegisteredMarkets(); - markets = await fetchMarkets({ + marketsPromise = fetchMarkets({ page, sortBy, orderBy, searchEmojis, pageSize: MARKETS_PER_PAGE, }); + numMarketsPromise = fetchNumRegisteredMarkets(); + } + + let featuredPromise: ReturnType; + + if (sortBy === SortMarketsBy.DailyVolume && orderBy === ORDER_BY.DESC) { + featuredPromise = marketsPromise.then((r) => r[0]); + } else { + featuredPromise = fetchFeaturedMarket(); } - const priceFeed = await fetchPriceFeed({}); + const [featured, priceFeed, markets, numMarkets] = await Promise.all([ + featuredPromise, + priceFeedPromise, + marketsPromise, + numMarketsPromise, + ]); return ( { } return res; }); + const state = await fetchMarketState({ searchEmojis: emojis }); if (state) { const { marketID } = state.market; const marketAddress = deriveEmojicoinPublisherAddress({ emojis }); - const chats = await fetchChatEvents({ marketID }); - const swaps = await fetchSwapEvents({ marketID }); - const marketView = await fetchContractMarketView(marketAddress.toString()); + + const [chats, swaps, marketView] = await Promise.all([ + fetchChatEvents({ marketID }), + fetchSwapEvents({ marketID }), + fetchContractMarketView(marketAddress.toString()), + ]); return ( { } }, [stateEvents]); - const { ref: bondingCurveRef } = useLabelScrambler(`${bondingProgress.toFixed(2)}%`, "%"); + const { ref: bondingCurveRef } = useLabelScrambler(`${bondingProgress.toFixed(2)}`, "%"); return (
diff --git a/src/typescript/frontend/src/components/pages/home/components/table-card/animation-variants/event-variants.ts b/src/typescript/frontend/src/components/pages/home/components/table-card/animation-variants/event-variants.ts index e0871b6ec..4166235e2 100644 --- a/src/typescript/frontend/src/components/pages/home/components/table-card/animation-variants/event-variants.ts +++ b/src/typescript/frontend/src/components/pages/home/components/table-card/animation-variants/event-variants.ts @@ -142,7 +142,7 @@ export const useLabelScrambler = (value: string, suffix: string = "") => { } const scrambler = useScramble({ - text: value, + text: value + suffix, ...scrambleConfig, ignore, }); From 866e9e097c3c547ebfc76faf05a33585751fe3ef Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:15:48 -0800 Subject: [PATCH 39/94] [ECO-2412] Make sure `LP` shows up on liquidity page (#352) --- .../components/trade-emojicoin/InputLabels.tsx | 8 ++++---- .../pages/pools/components/liquidity/index.tsx | 11 +++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/InputLabels.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/InputLabels.tsx index 254f0cefa..a4c0aecb4 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/InputLabels.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/InputLabels.tsx @@ -7,9 +7,9 @@ export const AptosInputLabel = () => (
); +export const EmojiInputLabelStyles = + "pixel-heading-3 text-light-gray text-[24px] md:text-[30px] cursor-default"; + export const EmojiInputLabel = ({ emoji }: { emoji: string }) => ( - + ); diff --git a/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx b/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx index 38c7d80e4..27b9d80a0 100644 --- a/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx +++ b/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx @@ -11,6 +11,7 @@ import { toCoinDecimalString } from "lib/utils/decimals"; import { AptosInputLabel, EmojiInputLabel, + EmojiInputLabelStyles, } from "components/pages/emojicoin/components/trade-emojicoin/InputLabels"; import { useAptos } from "context/wallet-context/AptosContextProvider"; import { toActualCoinDecimals } from "lib/utils/decimals"; @@ -216,7 +217,10 @@ const Liquidity = ({ market }: LiquidityProps) => { disabled > - +
+ + {market ? "" : "-"} +
); @@ -246,7 +250,10 @@ const Liquidity = ({ market }: LiquidityProps) => { /> )} - +
+ + {market ? " LP" : "-"} +
); From e323ba640290c450093cb5178926fa7d881d944f Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:48:03 -0800 Subject: [PATCH 40/94] [ECO-2415] Add `cherry-pick` strategy to the `README.md` (#356) --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/README.md b/README.md index 01986cb0b..f7049cdd4 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,69 @@ The process is as simple as merging feature branches to `main` first to trigger CI/CD checks, and then once it's merged into `main`, merging or [cherry-picking] a subset of the new features into the `production` branch. +If you don't have a production branch yet, simply run the following: + +1. `git checkout main && git pull origin main` +1. `git checkout -b production` +1. `git push origin production` + +Now a production branch exists. + +To merge new changes from `main` after `production` already exists, you can +create a PR that merges all new changes in `main` into `production`. + +### Cherry-picking strategy + +It's possible to preserve the linear history of `main` when merging into +`production` while still squashing the commits. This process looks like the +following: + +```shell +# Make sure `main` is up to date in your local environment. +git checkout main && git pull origin main + +# Do the same for `production`. +git checkout production && git pull origin production + +# Create a branch to make a PR to cherry-pick new changes from main into +# production with. +git checkout -b merge-main-to-prod +``` + +Now find the last commit that `main` and `production` share. If the commit +history looks like this: + +```yaml +- main: + - 06eadf3 + - 05eacbd + - 04de8fb # Squashed into the last commit `ff382ba` in `production`. + - 03aebcd # Squashed into the last commit `ff382ba` in `production`. + - 02abde4 # Common ancestor. + - 01ebadc +- production: + - ff382ba # Commit that previously squashed `03..` + `04..` into `production`. + - 02abde4 # Common ancestor. + - 01ebadc +``` + +Then the last commit they share (the common ancestor) is `02abde4`. + +Thus you should run: + +```shell +# Cherry-pick all commits from `02abde4..06eadf3` into `production`. +git cherry-pick 02abde4..06eadf3 + +# Push to the new branch. +git push origin merge-main-to-prod +``` + +Now you can make a PR to merge main to production with the `merge-main-to-prod` +branch! + +### Environment variables + You can set in Vercel project settings the production branch. By default, this is `main`, however you can use the `production` branch to separate staging environments from the release (aka production) environment. From 0e12a94911d9322f440eaef59a6502e5211b05ff Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:01:11 -0800 Subject: [PATCH 41/94] [ECO-2403] Parallelize docker image builds (#353) --- .github/workflows/push-broker.yaml | 7 ++++++- src/rust/processor | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push-broker.yaml b/.github/workflows/push-broker.yaml index ff1f66248..42f62675b 100644 --- a/.github/workflows/push-broker.yaml +++ b/.github/workflows/push-broker.yaml @@ -25,9 +25,14 @@ jobs: context: 'src/rust' file: 'src/rust/broker/Dockerfile' labels: '${{ steps.metadata.outputs.labels }}' - platforms: '${{ vars.DOCKER_IMAGE_PLATFORMS }}' + platforms: '${{ matrix.platform }}' push: 'true' tags: '${{ steps.metadata.outputs.tags }}' + strategy: + matrix: + platform: + - 'linux/amd64' + - 'linux/arm64' timeout-minutes: 360 name: 'Build broker Docker image and push to Docker Hub' 'on': diff --git a/src/rust/processor b/src/rust/processor index 9f60d2d4c..389fe7a00 160000 --- a/src/rust/processor +++ b/src/rust/processor @@ -1 +1 @@ -Subproject commit 9f60d2d4c38a3a86e49a52d6c10da67f204bb6b9 +Subproject commit 389fe7a00bc92294bc75a74cbddec28d27eb4b88 From cd89daaa6326edfb68cd6643ab17e4897585f84b Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:20:42 -0800 Subject: [PATCH 42/94] [ECO-2419] Bump stack image versions (#359) --- src/cloud-formation/deploy-indexer-main.yaml | 4 ++-- src/cloud-formation/deploy-indexer-production.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cloud-formation/deploy-indexer-main.yaml b/src/cloud-formation/deploy-indexer-main.yaml index 03a2d1ffd..a846b99fd 100644 --- a/src/cloud-formation/deploy-indexer-main.yaml +++ b/src/cloud-formation/deploy-indexer-main.yaml @@ -1,6 +1,6 @@ --- parameters: - BrokerImageVersion: '0.9.1' + BrokerImageVersion: '1.0.0' DeployAlb: 'true' DeployAlbDnsRecord: 'true' DeployBastionHost: 'true' @@ -20,7 +20,7 @@ parameters: EnableWafRulesWebSocket: 'false' Environment: 'main' Network: 'testnet' - ProcessorImageVersion: '0.9.1' + ProcessorImageVersion: '1.0.0' VpcStackName: 'emoji-vpc' tags: null template-file-path: 'src/cloud-formation/indexer.cfn.yaml' diff --git a/src/cloud-formation/deploy-indexer-production.yaml b/src/cloud-formation/deploy-indexer-production.yaml index 3371807b6..af844b796 100644 --- a/src/cloud-formation/deploy-indexer-production.yaml +++ b/src/cloud-formation/deploy-indexer-production.yaml @@ -1,6 +1,6 @@ --- parameters: - BrokerImageVersion: '0.9.1' + BrokerImageVersion: '1.0.0' DeployAlb: 'true' DeployAlbDnsRecord: 'true' DeployBastionHost: 'true' @@ -20,7 +20,7 @@ parameters: EnableWafRulesWebSocket: 'false' Environment: 'production' Network: 'testnet' - ProcessorImageVersion: '0.9.1' + ProcessorImageVersion: '1.0.0' VpcStackName: 'emoji-vpc' tags: null template-file-path: 'src/cloud-formation/indexer.cfn.yaml' From bd1ef5e7ae0ba69daeef8b6a95b6141d7bea82d1 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Sat, 16 Nov 2024 02:33:27 +0100 Subject: [PATCH 43/94] [ECO-2404] Add localStorage caching of events (#354) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- .../wallet-context/AptosContextProvider.tsx | 2 +- .../src/lib/store/event/event-store.ts | 43 +++- .../src/lib/store/event/local-storage.ts | 194 +++--------------- .../frontend/src/lib/store/event/types.ts | 2 +- .../frontend/src/lib/store/event/utils.ts | 10 + src/typescript/frontend/src/utils/index.ts | 10 +- 6 files changed, 88 insertions(+), 173 deletions(-) diff --git a/src/typescript/frontend/src/context/wallet-context/AptosContextProvider.tsx b/src/typescript/frontend/src/context/wallet-context/AptosContextProvider.tsx index bb6dd068f..a5c1cea63 100644 --- a/src/typescript/frontend/src/context/wallet-context/AptosContextProvider.tsx +++ b/src/typescript/frontend/src/context/wallet-context/AptosContextProvider.tsx @@ -242,7 +242,7 @@ export function AptosContextProvider({ children }: PropsWithChildren) { ...models.liquidityEvents, ...models.marketLatestStateEvents, ]; - flattenedEvents.forEach(pushEventFromClient); + flattenedEvents.forEach((e) => pushEventFromClient(e, true)); parseChangesAndSetBalances(response); } diff --git a/src/typescript/frontend/src/lib/store/event/event-store.ts b/src/typescript/frontend/src/lib/store/event/event-store.ts index cf0e62a29..6a7a81024 100644 --- a/src/typescript/frontend/src/lib/store/event/event-store.ts +++ b/src/typescript/frontend/src/lib/store/event/event-store.ts @@ -18,16 +18,22 @@ import { handleLatestBarForSwapEvent, pushPeriodicStateEvents, toMappedMarketEvents, + initialState, } from "./utils"; -import { initialStatePatch } from "./local-storage"; import { periodEnumToRawDuration } from "@sdk/const"; import { createWebSocketClientStore, type WebSocketClientStore } from "../websocket/store"; import { DEBUG_ASSERT, extractFilter } from "@sdk/utils"; +import { + updateLocalStorage, + cleanReadLocalStorage, + clearLocalStorage, + LOCAL_STORAGE_EVENT_TYPES, +} from "./local-storage"; export const createEventStore = () => { - return createStore()( + const store = createStore()( immer((set, get) => ({ - ...initialStatePatch(), + ...initialState(), getMarket: (emojis) => get().markets.get(emojis.join("")), getRegisteredMarkets: () => { return get().markets; @@ -77,7 +83,7 @@ export const createEventStore = () => { }); }); }, - pushEventFromClient: (event: AnyEventModel) => { + pushEventFromClient: (event: AnyEventModel, pushToLocalStorage = false) => { if (get().guids.has(event.guid)) return; set((state) => { state.guids.add(event.guid); @@ -91,17 +97,32 @@ export const createEventStore = () => { if (isSwapEventModel(event)) { market.swapEvents.unshift(event); handleLatestBarForSwapEvent(market, event); + if (pushToLocalStorage) { + updateLocalStorage("swap", event); + } } else if (isChatEventModel(event)) { market.chatEvents.unshift(event); + if (pushToLocalStorage) { + updateLocalStorage("chat", event); + } } else if (isLiquidityEventModel(event)) { market.liquidityEvents.unshift(event); + if (pushToLocalStorage) { + updateLocalStorage("liquidity", event); + } } else if (isMarketLatestStateEventModel(event)) { market.stateEvents.unshift(event); state.stateFirehose.unshift(event); + if (pushToLocalStorage) { + updateLocalStorage("market", event); + } } else if (isPeriodicStateEventModel(event)) { const period = periodEnumToRawDuration(event.periodicMetadata.period); market[period].candlesticks.unshift(event); handleLatestBarForPeriodicStateEvent(market, event); + if (pushToLocalStorage) { + updateLocalStorage("periodic", event); + } } } }); @@ -146,4 +167,18 @@ export const createEventStore = () => { ...createWebSocketClientStore(set, get), })) ); + + const state = store.getState(); + for (const eventType of LOCAL_STORAGE_EVENT_TYPES) { + try { + const events = cleanReadLocalStorage(eventType); + for (const event of events) { + state.pushEventFromClient(event); + } + } catch (e) { + console.error(e); + clearLocalStorage(eventType); + } + } + return store; }; diff --git a/src/typescript/frontend/src/lib/store/event/local-storage.ts b/src/typescript/frontend/src/lib/store/event/local-storage.ts index ac15f4a0f..93903a767 100644 --- a/src/typescript/frontend/src/lib/store/event/local-storage.ts +++ b/src/typescript/frontend/src/lib/store/event/local-storage.ts @@ -1,177 +1,39 @@ -import { - type DatabaseModels, - type AnyEventModel, - type MarketLatestStateEventModel, - type MarketRegistrationEventModel, - type GlobalStateEventModel, -} from "@sdk/indexer-v2/types"; -import { type TableName } from "@sdk/indexer-v2/types/json-types"; -import { LOCALSTORAGE_EXPIRY_TIME_MS } from "const"; +import { type AnyEventModel } from "@sdk/indexer-v2/types"; import { parseJSON, stringifyJSON } from "utils"; -import { type MarketEventStore, type EventState, type SymbolString } from "./types"; -import { createInitialMarketState } from "./utils"; -import { type DeepWritable } from "@sdk/utils/utility-types"; -const shouldKeepItem = (event: T) => { - const eventTime = Number(event.transaction.timestamp); - const now = new Date().getTime(); - return eventTime > now - LOCALSTORAGE_EXPIRY_TIME_MS; -}; +export const LOCAL_STORAGE_EXPIRATION_TIME = 1000 * 60; // 10 minutes. -export const addToLocalStorage = (event: T) => { - const { eventName } = event; - const events = localStorage.getItem(eventName) ?? "[]"; - const filtered: T[] = parseJSON(events).filter(shouldKeepItem); - const guids = new Set(filtered.map((e) => e.guid)); - if (!guids.has(event.guid)) { - filtered.push(event); - } - localStorage.setItem(eventName, stringifyJSON(filtered)); -}; +export const LOCAL_STORAGE_EVENT_TYPES = [ + "swap", + "chat", + "liquidity", + "market", + "periodic", +] as const; +type EventLocalStorageKey = (typeof LOCAL_STORAGE_EVENT_TYPES)[number]; -type EventArraysByModelType = { - Swap: Array; - Chat: Array; - MarketRegistration: Array; - PeriodicState: Array; - State: Array; - GlobalState: Array; - Liquidity: Array; +const shouldKeep = (e: AnyEventModel) => { + const now = new Date().getTime(); + // The time at which all events that occurred prior to are considered stale. + const staleTimeBoundary = new Date(now - LOCAL_STORAGE_EXPIRATION_TIME); + return e.transaction.timestamp > staleTimeBoundary; }; -const emptyEventArraysByModelType: () => EventArraysByModelType = () => ({ - Swap: [] as EventArraysByModelType["Swap"], - Chat: [] as EventArraysByModelType["Chat"], - MarketRegistration: [] as EventArraysByModelType["MarketRegistration"], - PeriodicState: [] as EventArraysByModelType["PeriodicState"], - State: [] as EventArraysByModelType["State"], - GlobalState: [] as EventArraysByModelType["GlobalState"], - Liquidity: [] as EventArraysByModelType["Liquidity"], -}); - -type MarketEventTypes = - | DatabaseModels["swap_events"] - | DatabaseModels["chat_events"] - | MarketLatestStateEventModel - | DatabaseModels["liquidity_events"] - | DatabaseModels["periodic_state_events"]; - -export const initialStatePatch = (): EventState => { - return { - guids: new Set(), - stateFirehose: [], - marketRegistrations: [], - markets: new Map(), - globalStateEvents: [], - }; +export const updateLocalStorage = (key: EventLocalStorageKey, event: AnyEventModel) => { + const str = localStorage.getItem(key) ?? "[]"; + const data: AnyEventModel[] = parseJSON(str); + data.unshift(event); + localStorage.setItem(key, stringifyJSON(data)); }; -export const initialStateFromLocalStorage = (): EventState => { - // Purge stale events then load up the remaining ones. - const events = getEventsFromLocalStorage(); - - // Sort each event that has a market by its market. - const markets: Map> = new Map(); - const guids: Set = new Set(); - - const addGuidAndGetMarket = (event: MarketEventTypes) => { - // Before ensuring the market is initialized, add the incoming event to the set of guids. - guids.add(event.guid); - - const { market } = event; - const symbol = market.symbolData.symbol; - if (!markets.has(symbol)) { - markets.set(symbol, createInitialMarketState(market)); - } - return markets.get(symbol)!; - }; - - const marketRegistrations: MarketRegistrationEventModel[] = []; - const globalStateEvents: GlobalStateEventModel[] = []; - - events.Chat.forEach((e) => { - addGuidAndGetMarket(e).chatEvents.push(e); - }); - events.Liquidity.forEach((e) => { - addGuidAndGetMarket(e).liquidityEvents.push(e); - }); - events.State.forEach((e) => { - addGuidAndGetMarket(e).stateEvents.push(e); - }); - events.Swap.forEach((e) => { - addGuidAndGetMarket(e).swapEvents.push(e); - }); - events.PeriodicState.forEach((e) => { - addGuidAndGetMarket(e)[e.periodicMetadata.period].candlesticks.push(e); - }); - events.MarketRegistration.forEach((e) => { - marketRegistrations.push(e); - guids.add(e.guid); - }); - events.GlobalState.forEach((e) => { - globalStateEvents.push(e); - guids.add(e.guid); - }); - - const stateFirehose: MarketLatestStateEventModel[] = []; - - for (const { stateEvents } of markets.values()) { - stateFirehose.push(...(stateEvents as Array)); - } - - // Sort the state firehose by bump time, then market ID, then market nonce. - stateFirehose.sort(({ market: a }, { market: b }) => { - if (a.time === b.time) { - if (a.marketID === b.marketID) { - if (a.marketNonce === b.marketNonce) return 0; - if (a.marketNonce < b.marketNonce) return 1; - return -1; - } else if (a.marketID < b.marketID) { - return 1; - } - return -1; - } else if (a.time < b.time) { - return -1; - } - return 1; - }); - - return { - guids, - stateFirehose, - marketRegistrations, - markets: markets as unknown as Map, - globalStateEvents, - }; +export const cleanReadLocalStorage = (key: EventLocalStorageKey) => { + const str = localStorage.getItem(key) ?? "[]"; + const data: AnyEventModel[] = parseJSON(str); + const relevantItems = data.filter(shouldKeep); + localStorage.setItem(key, stringifyJSON(relevantItems)); + return relevantItems; }; -/** - * Purges old local storage events and returns any that remain. - */ -export const getEventsFromLocalStorage = () => { - const res = emptyEventArraysByModelType(); - const guids = new Set(); - - // Filter the events in local storage, then return them. - Object.entries(res).forEach((entry) => { - const eventName = entry[0] as keyof EventArraysByModelType; - const existing = localStorage.getItem(eventName) ?? "[]"; - const filtered = - parseJSON(existing).filter(shouldKeepItem); - const reduced = filtered.reduce( - (acc, curr) => { - if (!guids.has(curr.guid)) { - acc.push(curr); - guids.add(curr.guid); - } - return acc; - }, - [] as typeof filtered - ); - const events = entry[1] as typeof filtered; - events.push(...reduced); - localStorage.setItem(eventName, stringifyJSON(filtered)); - }); - - return res; +export const clearLocalStorage = (key: EventLocalStorageKey) => { + localStorage.setItem(key, "[]"); }; diff --git a/src/typescript/frontend/src/lib/store/event/types.ts b/src/typescript/frontend/src/lib/store/event/types.ts index 4dacc886f..050360d74 100644 --- a/src/typescript/frontend/src/lib/store/event/types.ts +++ b/src/typescript/frontend/src/lib/store/event/types.ts @@ -68,7 +68,7 @@ export type EventActions = { getRegisteredMarkets: () => Readonly; loadMarketStateFromServer: (states: Array) => void; loadEventsFromServer: (events: Array) => void; - pushEventFromClient: (event: AnyEventModel) => void; + pushEventFromClient: (event: AnyEventModel, localize?: boolean) => void; setLatestBars: ({ marketMetadata, latestBars }: SetLatestBarsArgs) => void; subscribeToPeriod: ({ marketEmojis, period, cb }: PeriodSubscription) => void; unsubscribeFromPeriod: ({ marketEmojis, period }: Omit) => void; diff --git a/src/typescript/frontend/src/lib/store/event/utils.ts b/src/typescript/frontend/src/lib/store/event/utils.ts index 262dab87f..88d8f719e 100644 --- a/src/typescript/frontend/src/lib/store/event/utils.ts +++ b/src/typescript/frontend/src/lib/store/event/utils.ts @@ -162,3 +162,13 @@ export const toMappedMarketEvents = (events: Arr events.forEach((event) => map.get(event.market.symbolData.symbol)!.push(event)); return map; }; + +export const initialState = (): EventState => { + return { + guids: new Set(), + stateFirehose: [], + marketRegistrations: [], + markets: new Map(), + globalStateEvents: [], + }; +}; diff --git a/src/typescript/frontend/src/utils/index.ts b/src/typescript/frontend/src/utils/index.ts index b91da81a1..4eb2eb686 100644 --- a/src/typescript/frontend/src/utils/index.ts +++ b/src/typescript/frontend/src/utils/index.ts @@ -13,13 +13,21 @@ export { isDisallowedEventKey } from "./check-is-disallowed-event-key"; export { getEmptyListTr } from "./get-empty-list-tr"; export const stringifyJSON = (data: object) => - JSON.stringify(data, (_, value) => (typeof value === "bigint" ? value.toString() + "n" : value)); + JSON.stringify(data, (_, value) => { + if (typeof value === "bigint") return value.toString() + "n"; + return value; + }); export const parseJSON = (json: string): T => JSON.parse(json, (_, value) => { if (typeof value === "string" && /^\d+n$/.test(value)) { return BigInt(value.substring(0, value.length - 1)); } + // This matches the below pattern: 1234-12-31T23:59:59.666Z + const dateRegex = /^\d{4}-\d{2}-\d2T\d{2}:\d{2}:\d{2}.\d*Z$/; + if (typeof value === "string" && dateRegex.test(value)) { + return new Date(value); + } return value as T; }); From dec7a04e2935901ffe181897cd54ac321d8fd4d5 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 15 Nov 2024 18:04:55 -0800 Subject: [PATCH 44/94] =?UTF-8?q?[ECO-2418]=20Separate=20format=20from=20l?= =?UTF-8?q?int=20in=20pre-commit=20so=20IDE's=20don't=20show=20a=20million?= =?UTF-8?q?=E2=80=A6=20(#358)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cfg/pre-commit-config.yaml | 12 +++++++++++- src/typescript/frontend/.eslintrc.js | 4 +--- src/typescript/frontend/package.json | 4 ++-- src/typescript/sdk/.eslintrc.js | 4 +--- src/typescript/sdk/package.json | 4 ++-- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/cfg/pre-commit-config.yaml b/cfg/pre-commit-config.yaml index 42cb9d876..c409e5f16 100644 --- a/cfg/pre-commit-config.yaml +++ b/cfg/pre-commit-config.yaml @@ -109,11 +109,21 @@ repos: name: 'mypy' types: - 'python' + - + entry: | + pnpm run format:check + files: 'src/typescript' + id: 'ts-format' + language: 'node' + name: 'format typescript' + pass_filenames: false + types: + - 'directory' - entry: | pnpm run lint files: 'src/typescript' - id: 'pnpm' + id: 'ts-lint' language: 'node' name: 'lint typescript' pass_filenames: false diff --git a/src/typescript/frontend/.eslintrc.js b/src/typescript/frontend/.eslintrc.js index f133c9fa8..465035ae1 100644 --- a/src/typescript/frontend/.eslintrc.js +++ b/src/typescript/frontend/.eslintrc.js @@ -11,7 +11,6 @@ module.exports = { "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", - "prettier", ], globals: { JSX: true, @@ -39,9 +38,8 @@ module.exports = { project: ["tsconfig.json", "tests/tsconfig.json"], warnOnUnsupportedTypeScriptVersion: false, }, - plugins: ["@typescript-eslint", "import", "prettier"], + plugins: ["@typescript-eslint", "import"], rules: { - "prettier/prettier": ["error"], "import/no-cycle": [ "error", { diff --git a/src/typescript/frontend/package.json b/src/typescript/frontend/package.json index 5fe891992..79efd0246 100644 --- a/src/typescript/frontend/package.json +++ b/src/typescript/frontend/package.json @@ -88,13 +88,13 @@ "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a", "private": true, "scripts": { - "_format": "prettier -l './**/*.{js,jsx,ts,tsx,css,md}' --config ./.prettierrc.js", + "_format": "prettier './**/*.{js,jsx,ts,tsx,css,md}' --config ./.prettierrc.js", "build": "next build", "build:debug": "BUILD_DEBUG=true next build --no-lint --no-mangling --debug", "build:no-checks": "IGNORE_BUILD_ERRORS=true next build --no-lint", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist && rm -rf .next", "dev": "NODE_OPTIONS='--inspect' next dev --turbo --port 3001", - "format": "pnpm _format --write", + "format": "pnpm _format --list-different --write", "format:check": "pnpm _format --check", "lint": "eslint --max-warnings=0 -c .eslintrc.js --ext .js,.jsx,.ts,.tsx .", "lint:fix": "pnpm run lint --fix", diff --git a/src/typescript/sdk/.eslintrc.js b/src/typescript/sdk/.eslintrc.js index 676adc0d4..9e08c2931 100644 --- a/src/typescript/sdk/.eslintrc.js +++ b/src/typescript/sdk/.eslintrc.js @@ -9,7 +9,6 @@ module.exports = { extends: [ "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", - "prettier", ], parser: "@typescript-eslint/parser", parserOptions: { @@ -19,9 +18,8 @@ module.exports = { sourceType: "module", warnOnUnsupportedTypeScriptVersion: false, }, - plugins: ["@typescript-eslint", "unused-imports", "import", "prettier"], + plugins: ["@typescript-eslint", "unused-imports", "import"], rules: { - "prettier/prettier": ["error"], "@typescript-eslint/no-explicit-any": "warn", "no-console": [ "warn", diff --git a/src/typescript/sdk/package.json b/src/typescript/sdk/package.json index 70478deb5..e0fe663b6 100644 --- a/src/typescript/sdk/package.json +++ b/src/typescript/sdk/package.json @@ -53,14 +53,14 @@ "name": "@econia-labs/emojicoin-sdk", "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a", "scripts": { - "_format": "prettier -l 'src/**/*.ts' 'tests/**/*.ts' '.eslintrc.js'", + "_format": "prettier 'src/**/*.ts' 'tests/**/*.ts' '.eslintrc.js'", "build": "tsc", "build:debug": "BUILD_DEBUG=true pnpm run build", "build:no-checks": "tsc --skipLibCheck", "check": "tsc -p tests/tsconfig.json --noEmit", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", "e2e:testnet": "pnpm load-test-env -v NO_TEST_SETUP=true -- pnpm jest tests/e2e/queries/testnet", - "format": "pnpm _format --write", + "format": "pnpm _format --list-different --write", "format:check": "pnpm _format --check", "lint": "eslint --max-warnings=0 'src/**/*.ts' 'tests/**/*.ts' -c .eslintrc.js", "lint:fix": "pnpm lint --fix", From 86b6fc191b834d8af2f60a7933f1708a914321f1 Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Sat, 16 Nov 2024 21:02:17 -0800 Subject: [PATCH 45/94] [ECO-2420] Refactor claim link module for ease of DevOps (#360) --- .../sources/emojicoin_dot_fun_claim_link.move | 396 +++++++++++++++--- 1 file changed, 327 insertions(+), 69 deletions(-) diff --git a/src/move/rewards/sources/emojicoin_dot_fun_claim_link.move b/src/move/rewards/sources/emojicoin_dot_fun_claim_link.move index 031187360..00a2395a9 100644 --- a/src/move/rewards/sources/emojicoin_dot_fun_claim_link.move +++ b/src/move/rewards/sources/emojicoin_dot_fun_claim_link.move @@ -8,6 +8,7 @@ module rewards::emojicoin_dot_fun_claim_link { use aptos_framework::coin; use aptos_framework::event; use aptos_std::ed25519::{Self, ValidatedPublicKey}; + use aptos_std::from_bcs; use aptos_std::simple_map::SimpleMap; use aptos_std::smart_table::{Self, SmartTable}; use emojicoin_dot_fun::emojicoin_dot_fun::{Self, Swap}; @@ -16,7 +17,6 @@ module rewards::emojicoin_dot_fun_claim_link { use std::signer; const INTEGRATOR_FEE_RATE_BPS: u8 = 100; - const NIL: address = @0x0; const DEFAULT_CLAIM_AMOUNT: u64 = 100_000_000; const VAULT: vector = b"Claim link vault"; @@ -24,7 +24,7 @@ module rewards::emojicoin_dot_fun_claim_link { const E_NOT_ADMIN: u64 = 0; /// Admin to remove address does not correspond to admin. const E_ADMIN_TO_REMOVE_IS_NOT_ADMIN: u64 = 1; - /// Public key of claim link private key is not in manifest. + /// Public key of claim link private key is not eligible. const E_INVALID_CLAIM_LINK: u64 = 2; /// Claim link has already been claimed. const E_CLAIM_LINK_ALREADY_CLAIMED: u64 = 3; @@ -38,14 +38,21 @@ module rewards::emojicoin_dot_fun_claim_link { const E_ADMIN_TO_REMOVE_IS_REWARDS_PUBLISHER: u64 = 7; /// Admin is already an admin. const E_ALREADY_ADMIN: u64 = 8; + /// Claim link is already eligible. + const E_CLAIM_LINK_ALREADY_ELIGIBLE: u64 = 9; + + struct Nil {} + has copy, drop, store; struct Vault has key { - /// Addresses of signers who can mutate the manifest. + /// Addresses of signers who can mutate the vault. admins: vector
, /// In octas. claim_amount: u64, - /// Map from claim link public key to address of claimant, `NIL` if unclaimed. - manifest: SmartTable, + /// Eligible claim link public keys. + eligible: SmartTable, + /// Map from claim link public key to address of claimant. + claimed: SmartTable, /// Approves transfers from the vault. signer_capability: SignerCapability } @@ -68,32 +75,65 @@ module rewards::emojicoin_dot_fun_claim_link { } #[view] - public fun public_key_is_in_manifest(public_key_bytes: vector): bool acquires Vault { - Vault[@rewards].manifest.contains(validate_public_key_bytes(public_key_bytes)) + public fun public_key_claimant(public_key_bytes: vector): Option
acquires Vault { + let validated_public_key_option = + ed25519::new_validated_public_key_from_bytes(public_key_bytes); + if (option::is_some(&validated_public_key_option)) { + let validated_public_key = option::destroy_some(validated_public_key_option); + let claimed_ref = &Vault[@rewards].claimed; + if (claimed_ref.contains(validated_public_key)) { + option::some(*claimed_ref.borrow(validated_public_key)) + } else { + option::none() + } + } else { + option::none() + } } #[view] - public fun public_key_claimant(public_key_bytes: vector): address acquires Vault { - *Vault[@rewards].manifest.borrow(validate_public_key_bytes(public_key_bytes)) + public fun public_key_is_eligible(public_key_bytes: vector): bool acquires Vault { + let validated_public_key_option = + ed25519::new_validated_public_key_from_bytes(public_key_bytes); + if (option::is_some(&validated_public_key_option)) { + Vault[@rewards].eligible.contains( + option::destroy_some(validated_public_key_option) + ) + } else { false } } #[view] - public fun public_keys(): vector acquires Vault { - Vault[@rewards].manifest.keys() + public fun public_keys_that_are_claimed(): vector acquires Vault { + Vault[@rewards].claimed.keys() } #[view] - public fun public_keys_paginated( + public fun public_keys_that_are_claimed_paginated( starting_bucket_index: u64, starting_vector_index: u64, num_public_keys_to_get: u64 ): (vector, Option, Option) acquires Vault { - Vault[@rewards].manifest.keys_paginated( + Vault[@rewards].claimed.keys_paginated( starting_bucket_index, starting_vector_index, num_public_keys_to_get ) } #[view] - public fun manifest_to_simple_map(): SimpleMap acquires Vault { - Vault[@rewards].manifest.to_simple_map() + public fun public_keys_that_are_claimed_to_simple_map(): + SimpleMap acquires Vault { + Vault[@rewards].claimed.to_simple_map() + } + + #[view] + public fun public_keys_that_are_eligible(): vector acquires Vault { + Vault[@rewards].eligible.keys() + } + + #[view] + public fun public_keys_that_are_eligible_paginated( + starting_bucket_index: u64, starting_vector_index: u64, num_public_keys_to_get: u64 + ): (vector, Option, Option) acquires Vault { + Vault[@rewards].eligible.keys_paginated( + starting_bucket_index, starting_vector_index, num_public_keys_to_get + ) } #[view] @@ -117,10 +157,60 @@ module rewards::emojicoin_dot_fun_claim_link { public entry fun add_public_keys( admin: &signer, public_keys_as_bytes: vector> ) acquires Vault { - let manifest_ref_mut = &mut borrow_vault_mut_checked(admin).manifest; - public_keys_as_bytes.for_each(|public_key_bytes| { - manifest_ref_mut.add(validate_public_key_bytes(public_key_bytes), NIL); - }); + let vault_ref_mut = borrow_vault_mut_checked(admin); + let claimed_ref = &vault_ref_mut.claimed; + let eligible_ref_mut = &mut vault_ref_mut.eligible; + let validated_public_key; + public_keys_as_bytes.for_each_ref( + |public_key_bytes_ref| { + validated_public_key = validate_public_key_bytes(*public_key_bytes_ref); + assert!( + !claimed_ref.contains(validated_public_key), + E_CLAIM_LINK_ALREADY_CLAIMED + ); + assert!( + !eligible_ref_mut.contains(validated_public_key), + E_CLAIM_LINK_ALREADY_ELIGIBLE + ); + eligible_ref_mut.add(validated_public_key, Nil {}); + } + ); + } + + public entry fun add_public_keys_and_fund_gas_escrows( + admin: &signer, public_keys_as_bytes: vector>, amount_per_escrow: u64 + ) acquires Vault { + let vault_ref_mut = borrow_vault_mut_checked(admin); + let claimed_ref = &vault_ref_mut.claimed; + let eligible_ref_mut = &mut vault_ref_mut.eligible; + let coins = + coin::withdraw( + admin, public_keys_as_bytes.length() * amount_per_escrow + ); + let validated_public_key; + public_keys_as_bytes.for_each_ref( + |public_key_bytes_ref| { + validated_public_key = validate_public_key_bytes(*public_key_bytes_ref); + assert!( + !claimed_ref.contains(validated_public_key), + E_CLAIM_LINK_ALREADY_CLAIMED + ); + assert!( + !eligible_ref_mut.contains(validated_public_key), + E_CLAIM_LINK_ALREADY_ELIGIBLE + ); + eligible_ref_mut.add(validated_public_key, Nil {}); + aptos_account::deposit_coins( + from_bcs::to_address( + ed25519::validated_public_key_to_authentication_key( + &validated_public_key + ) + ), + coin::extract(&mut coins, amount_per_escrow) + ) + } + ); + coin::destroy_zero(coins); } public entry fun fund_vault(funder: &signer, n_claims: u64) acquires Vault { @@ -156,12 +246,13 @@ module rewards::emojicoin_dot_fun_claim_link { // Verify public key is eligible for claim. let vault_ref_mut = &mut Vault[@rewards]; - let manifest_ref_mut = &mut vault_ref_mut.manifest; - assert!(manifest_ref_mut.contains(validated_public_key), E_INVALID_CLAIM_LINK); + let claimed_ref_mut = &mut vault_ref_mut.claimed; + let eligible_ref_mut = &mut vault_ref_mut.eligible; assert!( - *manifest_ref_mut.borrow(validated_public_key) == NIL, + !claimed_ref_mut.contains(validated_public_key), E_CLAIM_LINK_ALREADY_CLAIMED ); + assert!(eligible_ref_mut.contains(validated_public_key), E_INVALID_CLAIM_LINK); // Check vault balance. let vault_signer_cap_ref = &vault_ref_mut.signer_capability; @@ -172,8 +263,9 @@ module rewards::emojicoin_dot_fun_claim_link { E_VAULT_INSUFFICIENT_FUNDS ); - // Update manifest, transfer APT to claimant. - *manifest_ref_mut.borrow_mut(validated_public_key) = claimant_address; + // Update tables, transfer APT to claimant. + eligible_ref_mut.remove(validated_public_key); + claimed_ref_mut.add(validated_public_key, claimant_address); let vault_signer = account::create_signer_with_capability(vault_signer_cap_ref); aptos_account::transfer(&vault_signer, claimant_address, claim_amount); @@ -218,12 +310,12 @@ module rewards::emojicoin_dot_fun_claim_link { public entry fun remove_public_keys( admin: &signer, public_keys_as_bytes: vector> ) acquires Vault { - let manifest_ref_mut = &mut borrow_vault_mut_checked(admin).manifest; - public_keys_as_bytes.for_each(|public_key_bytes| { - let validated_public_key = validate_public_key_bytes(public_key_bytes); - if (manifest_ref_mut.contains(validated_public_key) - && *manifest_ref_mut.borrow(validated_public_key) == NIL) { - manifest_ref_mut.remove(validated_public_key); + let eligible_ref_mut = &mut borrow_vault_mut_checked(admin).eligible; + let validated_public_key; + public_keys_as_bytes.for_each_ref(|public_key_bytes_ref| { + validated_public_key = validate_public_key_bytes(*public_key_bytes_ref); + if (eligible_ref_mut.contains(validated_public_key)) { + eligible_ref_mut.remove(validated_public_key); } }); } @@ -250,7 +342,8 @@ module rewards::emojicoin_dot_fun_claim_link { Vault { admins: vector[signer::address_of(rewards)], claim_amount: DEFAULT_CLAIM_AMOUNT, - manifest: smart_table::new(), + claimed: smart_table::new(), + eligible: smart_table::new(), signer_capability } ); @@ -331,6 +424,30 @@ module rewards::emojicoin_dot_fun_claim_link { add_admin(¬_admin_signer, not_admin); } + #[test, expected_failure(abort_code = E_CLAIM_LINK_ALREADY_CLAIMED)] + fun test_add_public_keys_claim_link_already_claimed() acquires Vault { + let (signature_bytes, claim_link_validated_public_key_bytes) = + prepare_for_redemption(); + redeem( + &get_signer(CLAIMANT), + signature_bytes, + claim_link_validated_public_key_bytes, + @black_cat_market, + 1 + ); + add_public_keys( + &get_signer(@rewards), vector[claim_link_validated_public_key_bytes] + ); + } + + #[test, expected_failure(abort_code = E_CLAIM_LINK_ALREADY_ELIGIBLE)] + fun test_add_public_keys_claim_link_already_eligible() acquires Vault { + let (_, claim_link_validated_public_key_bytes) = prepare_for_redemption(); + add_public_keys( + &get_signer(@rewards), vector[claim_link_validated_public_key_bytes] + ); + } + #[test, expected_failure(abort_code = E_INVALID_PUBLIC_KEY)] fun test_add_public_keys_invalid_public_key() acquires Vault { emojicoin_dot_fun::tests::init_package(); @@ -349,12 +466,121 @@ module rewards::emojicoin_dot_fun_claim_link { add_public_keys(¬_admin_signer, vector[]); } + #[test] + fun test_add_public_keys_and_fund_gas_escrows() acquires Vault { + // Prepare escrow account public keys. + let n_escrows = 3; + let amount_per_escrow = 2; + let escrow_account_public_keys = vector[]; + let escrow_account_public_key_bytes = vector[]; + let validated_public_key; + for (i in 0..n_escrows) { + (_, validated_public_key) = ed25519::generate_keys(); + escrow_account_public_key_bytes.push_back( + ed25519::validated_public_key_to_bytes(&validated_public_key) + ); + escrow_account_public_keys.push_back(validated_public_key); + }; + + // Init packages. + emojicoin_dot_fun::tests::init_package_then_exact_transition(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + + // Fund escrows. + emojicoin_dot_fun::test_acquisitions::mint_aptos_coin_to( + @rewards, n_escrows * amount_per_escrow + ); + add_public_keys_and_fund_gas_escrows( + &rewards_signer, escrow_account_public_key_bytes, amount_per_escrow + ); + + // Verify state. + let public_key_bytes; + escrow_account_public_key_bytes.for_each_ref(|public_key_bytes_ref| { + public_key_bytes = *public_key_bytes_ref; + assert!(public_key_is_eligible(public_key_bytes)); + }); + escrow_account_public_keys.for_each_ref(|public_key_ref| { + assert!( + coin::balance( + from_bcs::to_address( + ed25519::validated_public_key_to_authentication_key(public_key_ref) + ) + ) == amount_per_escrow + ); + }); + + // Call with zero public keys argument to invoke silent return. + assert!(coin::balance(@rewards) == 0); + add_public_keys_and_fund_gas_escrows( + &rewards_signer, vector[], amount_per_escrow + ); + assert!(coin::balance(@rewards) == 0); + + } + + #[test, expected_failure(abort_code = E_CLAIM_LINK_ALREADY_CLAIMED)] + fun test_add_public_keys_and_fund_gas_escrows_claim_link_already_claimed() acquires Vault { + let (signature_bytes, claim_link_validated_public_key_bytes) = + prepare_for_redemption(); + redeem( + &get_signer(CLAIMANT), + signature_bytes, + claim_link_validated_public_key_bytes, + @black_cat_market, + 1 + ); + let amount_per_escrow = 1; + emojicoin_dot_fun::test_acquisitions::mint_aptos_coin_to( + @rewards, amount_per_escrow + ); + add_public_keys_and_fund_gas_escrows( + &get_signer(@rewards), + vector[claim_link_validated_public_key_bytes], + amount_per_escrow + ); + } + + #[test, expected_failure(abort_code = E_CLAIM_LINK_ALREADY_ELIGIBLE)] + fun test_add_public_keys_and_fund_gas_escrows_claim_link_already_eligible() acquires Vault { + let (_, claim_link_validated_public_key_bytes) = prepare_for_redemption(); + let amount_per_escrow = 1; + emojicoin_dot_fun::test_acquisitions::mint_aptos_coin_to( + @rewards, amount_per_escrow + ); + add_public_keys_and_fund_gas_escrows( + &get_signer(@rewards), + vector[claim_link_validated_public_key_bytes], + amount_per_escrow + ); + } + + #[test, expected_failure(abort_code = E_INVALID_PUBLIC_KEY)] + fun test_add_public_keys_and_fund_gas_escrows_invalid_public_key() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + emojicoin_dot_fun::test_acquisitions::mint_aptos_coin_to(@rewards, 1); + init_module(&rewards_signer); + add_public_keys_and_fund_gas_escrows(&rewards_signer, vector[vector[0x0]], 1); + } + + #[test, expected_failure(abort_code = E_NOT_ADMIN)] + fun test_add_public_keys_and_fund_gas_escrows_not_admin() acquires Vault { + emojicoin_dot_fun::tests::init_package(); + let rewards_signer = get_signer(@rewards); + init_module(&rewards_signer); + let not_admin_signer = get_signer(@0x2222); + assert!(&rewards_signer != ¬_admin_signer); + add_public_keys_and_fund_gas_escrows(¬_admin_signer, vector[], 1); + } + #[test] fun test_general_flow() acquires Vault { // Initialize black cat market, have it undergo state transition. emojicoin_dot_fun::tests::init_package_then_exact_transition(); - // Get claim link private, public keys. + // Get claim link private, public keys, bogus public key. let (claim_link_private_key, claim_link_validated_public_key) = ed25519::generate_keys(); let claim_link_validated_public_key_bytes = @@ -367,14 +593,29 @@ module rewards::emojicoin_dot_fun_claim_link { // Check initial state. assert!(admins() == vector[@rewards]); assert!(claim_amount() == DEFAULT_CLAIM_AMOUNT); - assert!(!public_key_is_in_manifest(claim_link_validated_public_key_bytes)); - assert!(public_keys().is_empty()); + assert!(!public_key_is_eligible(claim_link_validated_public_key_bytes)); + assert!(!public_key_is_eligible(vector[0])); + assert!( + public_key_claimant(claim_link_validated_public_key_bytes) + == option::none() + ); + assert!( + public_key_claimant(vector[0]) == option::none() + ); + assert!(public_keys_that_are_claimed().is_empty()); + assert!(public_keys_that_are_eligible().is_empty()); let (keys, starting_bucket_index, starting_vector_index) = - public_keys_paginated(0, 0, 1); + public_keys_that_are_claimed_paginated(0, 0, 1); + assert!(keys == vector[]); + assert!(starting_bucket_index == option::none()); + assert!(starting_vector_index == option::none()); + (keys, starting_bucket_index, starting_vector_index) = public_keys_that_are_eligible_paginated( + 0, 0, 1 + ); assert!(keys == vector[]); assert!(starting_bucket_index == option::none()); assert!(starting_vector_index == option::none()); - assert!(manifest_to_simple_map().length() == 0); + assert!(public_keys_that_are_claimed_to_simple_map().length() == 0); assert!(vault_balance() == 0); assert!( vault_signer_address() @@ -399,20 +640,32 @@ module rewards::emojicoin_dot_fun_claim_link { // Check new state. assert!(admins() == vector[@rewards, new_admin]); assert!(claim_amount() == DEFAULT_CLAIM_AMOUNT); - assert!(public_key_is_in_manifest(claim_link_validated_public_key_bytes)); + assert!(public_key_is_eligible(claim_link_validated_public_key_bytes)); assert!( - manifest_to_simple_map().keys() == vector[claim_link_validated_public_key] + public_key_claimant(claim_link_validated_public_key_bytes) + == option::none() ); - assert!(public_key_claimant(claim_link_validated_public_key_bytes) == NIL); - (keys, starting_bucket_index, starting_vector_index) = public_keys_paginated( + assert!( + public_key_claimant(claim_link_validated_public_key_bytes) + == option::none() + ); + assert!( + public_key_claimant(vector[0]) == option::none() + ); + assert!( + public_keys_that_are_eligible() == vector[claim_link_validated_public_key] + ); + assert!( + public_key_claimant(claim_link_validated_public_key_bytes) + == option::none() + ); + (keys, starting_bucket_index, starting_vector_index) = public_keys_that_are_eligible_paginated( 0, 0, 1 ); assert!(keys == vector[claim_link_validated_public_key]); assert!(starting_bucket_index == option::none()); assert!(starting_vector_index == option::none()); - assert!( - manifest_to_simple_map().keys() == vector[claim_link_validated_public_key] - ); + assert!(public_keys_that_are_claimed_to_simple_map().length() == 0); assert!(vault_balance() == DEFAULT_CLAIM_AMOUNT); // Fund another reward, double claim amount, fund another reward, remove admin, withdraw. @@ -438,13 +691,12 @@ module rewards::emojicoin_dot_fun_claim_link { &rewards_signer, vector[claim_link_validated_public_key_bytes] ); - assert!(!public_key_is_in_manifest(claim_link_validated_public_key_bytes)); + assert!(!public_key_is_eligible(claim_link_validated_public_key_bytes)); add_public_keys( &rewards_signer, vector[claim_link_validated_public_key_bytes] ); - assert!(public_key_is_in_manifest(claim_link_validated_public_key_bytes)); - assert!(public_key_claimant(claim_link_validated_public_key_bytes) == NIL); + assert!(public_key_is_eligible(claim_link_validated_public_key_bytes)); // Get expected proceeds from swap. let swap_event = @@ -476,37 +728,43 @@ module rewards::emojicoin_dot_fun_claim_link { // Verify claimant's emojicoin balance. assert!(coin::balance(CLAIMANT) == net_proceeds); - // Check vault balance, manifest. + // Check vault balance, state. assert!(vault_balance() == 0); - assert!(public_key_claimant(claim_link_validated_public_key_bytes) == CLAIMANT); + assert!( + public_key_claimant(claim_link_validated_public_key_bytes) + == option::some(CLAIMANT) + ); + (keys, starting_bucket_index, starting_vector_index) = public_keys_that_are_claimed_paginated( + 0, 0, 1 + ); + assert!(keys == vector[claim_link_validated_public_key]); + assert!(starting_bucket_index == option::none()); + assert!(starting_vector_index == option::none()); + assert!( + public_keys_that_are_claimed_to_simple_map().keys() + == vector[claim_link_validated_public_key] + ); + assert!( + public_keys_that_are_claimed_to_simple_map().values() == vector[CLAIMANT] + ); // Verify that public key entry can no longer be removed. remove_public_keys( &rewards_signer, vector[claim_link_validated_public_key_bytes] ); - assert!(public_key_is_in_manifest(claim_link_validated_public_key_bytes)); - - // Verify silent return for trying to remove public key not in manifest. - let (_, new_public_key) = ed25519::generate_keys(); - remove_public_keys( - &rewards_signer, - vector[ed25519::validated_public_key_to_bytes(&new_public_key)] + assert!( + public_key_claimant(claim_link_validated_public_key_bytes) + == option::some(CLAIMANT) ); - } - #[test, expected_failure(abort_code = E_INVALID_PUBLIC_KEY)] - fun test_public_key_claimant_invalid_public_key() acquires Vault { - let (_, claim_link_validated_public_key_bytes) = prepare_for_redemption(); - claim_link_validated_public_key_bytes.push_back(0); - public_key_claimant(claim_link_validated_public_key_bytes); - } - - #[test, expected_failure(abort_code = E_INVALID_PUBLIC_KEY)] - fun test_public_key_is_in_manifest_invalid_public_key() acquires Vault { - let (_, claim_link_validated_public_key_bytes) = prepare_for_redemption(); - claim_link_validated_public_key_bytes.push_back(0); - public_key_is_in_manifest(claim_link_validated_public_key_bytes); + // Verify silent return for trying to remove public key that is not eligible. + let (_, new_public_key) = ed25519::generate_keys(); + let new_public_key_bytes = + ed25519::validated_public_key_to_bytes(&new_public_key); + remove_public_keys(&rewards_signer, vector[new_public_key_bytes]); + assert!(public_key_claimant(new_public_key_bytes) == option::none()); + assert!(!public_key_is_eligible(new_public_key_bytes)); } #[test, expected_failure(abort_code = E_CLAIM_LINK_ALREADY_CLAIMED)] @@ -645,7 +903,7 @@ module rewards::emojicoin_dot_fun_claim_link { } #[test, expected_failure(abort_code = E_NOT_ADMIN)] - fun withdraw_from_vault_not_admin() acquires Vault { + fun test_withdraw_from_vault_not_admin() acquires Vault { emojicoin_dot_fun::tests::init_package(); let rewards_signer = get_signer(@rewards); init_module(&rewards_signer); From a5160d69e736d641f522b63dcd82a3a566fd2539 Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:33:10 -0800 Subject: [PATCH 46/94] [ECO-2424] Refactor broker image build action to avoid overwriting (#365) --- .github/workflows/push-broker.yaml | 43 +++++++++++++++++++++++------- src/rust/processor | 2 +- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/.github/workflows/push-broker.yaml b/.github/workflows/push-broker.yaml index 42f62675b..4033e49b9 100644 --- a/.github/workflows/push-broker.yaml +++ b/.github/workflows/push-broker.yaml @@ -1,3 +1,5 @@ +# cspell:word imagetools +# cspell:word onlatest --- jobs: build-push: @@ -11,30 +13,53 @@ jobs: images: 'econialabs/emojicoin-dot-fun-indexer-broker' tags: | type=match,pattern=broker-v(.*),group=1 + - id: 'arm64-metadata' + uses: 'docker/metadata-action@v5' + with: + flavor: | + suffix=-arm64,onlatest=true + images: 'econialabs/emojicoin-dot-fun-indexer-broker' + tags: | + type=match,pattern=broker-v(.*),group=1 - uses: 'docker/setup-qemu-action@v3' - uses: 'docker/setup-buildx-action@v3' - uses: 'docker/login-action@v3' with: password: '${{ secrets.DOCKERHUB_TOKEN }}' username: '${{ secrets.DOCKERHUB_USERNAME }}' - - uses: 'docker/build-push-action@v6' + - name: 'Push AMD image to Docker Hub' + uses: 'docker/build-push-action@v6' with: build-args: 'FEATURES=ws' cache-from: 'type=gha' cache-to: 'type=gha,mode=max' context: 'src/rust' file: 'src/rust/broker/Dockerfile' - labels: '${{ steps.metadata.outputs.labels }}' - platforms: '${{ matrix.platform }}' + platforms: 'linux/amd64' push: 'true' tags: '${{ steps.metadata.outputs.tags }}' - strategy: - matrix: - platform: - - 'linux/amd64' - - 'linux/arm64' + - name: 'Clear Docker cache to free up space for ARM build' + run: 'docker system prune -af' + - name: 'Push ARM image to Docker Hub' + uses: 'docker/build-push-action@v6' + with: + build-args: 'FEATURES=ws' + cache-from: 'type=gha' + cache-to: 'type=gha,mode=max' + context: 'src/rust' + file: 'src/rust/broker/Dockerfile' + platforms: 'linux/arm64' + push: 'true' + tags: '${{ steps.arm64-metadata.outputs.tags }}' + - name: 'Append ARM images to AMD manifest' + run: | + echo "${{ steps.metadata.outputs.tags }}" | while read -r tag; do + if [ ! -z "$tag" ]; then + docker buildx imagetools create --append -t "$tag" "${tag}-arm64" + fi + done timeout-minutes: 360 -name: 'Build broker Docker image and push to Docker Hub' +name: 'Push multi-platform processor image to Docker Hub' 'on': push: tags: diff --git a/src/rust/processor b/src/rust/processor index 389fe7a00..84e72a0d5 160000 --- a/src/rust/processor +++ b/src/rust/processor @@ -1 +1 @@ -Subproject commit 389fe7a00bc92294bc75a74cbddec28d27eb4b88 +Subproject commit 84e72a0d5a9fac85d5102bea63e63cee4d056496 From 8b89c66dd773bb5aaf8e62c0a9329311b1432800 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:44:40 -0800 Subject: [PATCH 47/94] [ECO-2430] Build `deployer` service with Aptos CLI `v4.4.0` (#366) --- src/docker/deployer/Dockerfile | 2 +- src/docker/deployer/sh/build-publish-payloads.sh | 3 ++- src/docker/deployer/sh/init-profile.sh | 15 ++++++++------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/docker/deployer/Dockerfile b/src/docker/deployer/Dockerfile index 92d556eb7..44c010ef6 100644 --- a/src/docker/deployer/Dockerfile +++ b/src/docker/deployer/Dockerfile @@ -5,7 +5,7 @@ # the `yq` releases on apt are outdated and technically deprecated. FROM mikefarah/yq:4.44.3 AS yq -FROM econialabs/aptos-cli:4.1.0 +FROM econialabs/aptos-cli:4.4.0 COPY --from=yq /usr/bin/yq /usr/bin/yq diff --git a/src/docker/deployer/sh/build-publish-payloads.sh b/src/docker/deployer/sh/build-publish-payloads.sh index a30893660..987cd74ec 100644 --- a/src/docker/deployer/sh/build-publish-payloads.sh +++ b/src/docker/deployer/sh/build-publish-payloads.sh @@ -28,4 +28,5 @@ aptos move build-publish-payload \ --included-artifacts none \ --package-dir $move_dir/rewards/ \ --json-output-file $json_dir/rewards.json \ - --skip-fetch-latest-git-deps + --skip-fetch-latest-git-deps \ + --move-2 diff --git a/src/docker/deployer/sh/init-profile.sh b/src/docker/deployer/sh/init-profile.sh index 7fa86f966..15a8a1425 100644 --- a/src/docker/deployer/sh/init-profile.sh +++ b/src/docker/deployer/sh/init-profile.sh @@ -19,7 +19,7 @@ fi # This script initializes a profile on the `testnet` network and then updates # the profile to use the `custom` network with the correct rest and faucet URLs. # This is a workaround to avoid having to run a local testnet during the image -# build process. +# build process. If the testnet query fails, it tries to use mainnet. # This facilitates checking the derived address against the expected address at # build time and initializing the profile and subsequent `aptos` config.yaml # file without having to run a local testnet. @@ -29,12 +29,13 @@ fi profile="publisher" # See the note above for why we use `testnet` below. -result_json=$(aptos init \ - --assume-yes \ - --profile $profile \ - --private-key $PUBLISHER_PRIVATE_KEY \ - --encoding hex \ - --network testnet 2>/dev/null) +# Use `mainnet` if `testnet` fails. +base_cmd="aptos init --assume-yes --encoding hex" +cmd="$base_cmd --profile $profile --private-key $PUBLISHER_PRIVATE_KEY" +result_json=$( + $cmd --network testnet 2>/dev/null || + $cmd --network mainnet 2>/dev/null +) result=$(echo $result_json | jq -r '.Error') From 6e647a65487e48c97171bcd33ebfd94c68d63a93 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:52:14 -0800 Subject: [PATCH 48/94] =?UTF-8?q?[ECO-2429]=20Move=20the=20`emoji=20=3D>?= =?UTF-8?q?=20name`=20middleware=20conversion=20to=20be=20before=20the=20a?= =?UTF-8?q?llow=E2=80=A6=20(#364)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bogdan Crisan --- src/typescript/frontend/src/middleware.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/typescript/frontend/src/middleware.ts b/src/typescript/frontend/src/middleware.ts index 8c3023ed2..11fba00f5 100644 --- a/src/typescript/frontend/src/middleware.ts +++ b/src/typescript/frontend/src/middleware.ts @@ -21,10 +21,9 @@ export default async function middleware(request: NextRequest) { return NextResponse.next(); } - if (!IS_ALLOWLIST_ENABLED) { - return NextResponse.next(); - } - + // This will replace emojis in the path name with their actual text names. Since this occurs + // before the allowlist check, it will redirect the user to the pure text version of their path + // but then still require them to verify after. const possibleMarketPath = normalizePossibleMarketPath(pathname, request.url); if (possibleMarketPath) { return NextResponse.redirect(possibleMarketPath); From 4008e2e13068869b379b47576f7ba50a0af02803 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Mon, 18 Nov 2024 06:58:32 +0100 Subject: [PATCH 49/94] [ECO-2426] Remove Telegram references (#363) --- src/typescript/example.env | 1 - .../src/components/footer/constants.tsx | 2 -- .../mobile-social-links/constants.tsx | 2 -- .../src/components/svg/icons/Telegram.tsx | 19 ------------------- .../svg/icons/TelegramOutlineIcon.tsx | 19 ------------------- src/typescript/frontend/src/lib/env.ts | 1 - 6 files changed, 44 deletions(-) delete mode 100644 src/typescript/frontend/src/components/svg/icons/Telegram.tsx delete mode 100644 src/typescript/frontend/src/components/svg/icons/TelegramOutlineIcon.tsx diff --git a/src/typescript/example.env b/src/typescript/example.env index 6b5a6ffae..1ad12d558 100644 --- a/src/typescript/example.env +++ b/src/typescript/example.env @@ -28,7 +28,6 @@ NEXT_PUBLIC_LINKS='{ "x": "", "github": "", "discord": "", - "telegram": "", "tos": "" }' diff --git a/src/typescript/frontend/src/components/footer/constants.tsx b/src/typescript/frontend/src/components/footer/constants.tsx index f730e69c7..066e0f79c 100644 --- a/src/typescript/frontend/src/components/footer/constants.tsx +++ b/src/typescript/frontend/src/components/footer/constants.tsx @@ -1,11 +1,9 @@ import Discord from "@icons/Discord"; -import Telegram from "@icons/Telegram"; import Twitter from "components/svg/icons/Twitter"; import { LINKS } from "lib/env"; import { ROUTES } from "router/routes"; export const SOCIAL_ICONS = [ { icon: Discord, href: LINKS?.discord ?? ROUTES.notFound }, - { icon: Telegram, href: LINKS?.telegram ?? ROUTES.notFound }, { icon: Twitter, href: LINKS?.x ?? ROUTES.notFound }, ]; diff --git a/src/typescript/frontend/src/components/header/components/mobile-menu/components/mobile-social-links/constants.tsx b/src/typescript/frontend/src/components/header/components/mobile-menu/components/mobile-social-links/constants.tsx index cc90501f1..776dd354d 100644 --- a/src/typescript/frontend/src/components/header/components/mobile-menu/components/mobile-social-links/constants.tsx +++ b/src/typescript/frontend/src/components/header/components/mobile-menu/components/mobile-social-links/constants.tsx @@ -1,11 +1,9 @@ import TwitterOutlineIcon from "components/svg/icons/TwitterOutlineIcon"; import { LINKS } from "lib/env"; import DiscordOutlineIcon from "@icons/DiscordOutlineIcon"; -import TelegramOutlineIcon from "@icons/TelegramOutlineIcon"; import { ROUTES } from "router/routes"; export const SOCIAL_ICONS = [ { icon: DiscordOutlineIcon, href: LINKS?.discord ?? ROUTES.notFound }, - { icon: TelegramOutlineIcon, href: LINKS?.telegram ?? ROUTES.notFound }, { icon: TwitterOutlineIcon, href: LINKS?.x ?? ROUTES.notFound }, ]; diff --git a/src/typescript/frontend/src/components/svg/icons/Telegram.tsx b/src/typescript/frontend/src/components/svg/icons/Telegram.tsx deleted file mode 100644 index f13678a8e..000000000 --- a/src/typescript/frontend/src/components/svg/icons/Telegram.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import Svg from "components/svg/Svg"; -import { type SvgProps } from "../types"; -import { useThemeContext } from "context/theme-context"; - -const Icon: React.FC = ({ color = "black", ...props }) => { - const { theme } = useThemeContext(); - return ( - - - - - ); -}; - -export default Icon; diff --git a/src/typescript/frontend/src/components/svg/icons/TelegramOutlineIcon.tsx b/src/typescript/frontend/src/components/svg/icons/TelegramOutlineIcon.tsx deleted file mode 100644 index 3aa99291b..000000000 --- a/src/typescript/frontend/src/components/svg/icons/TelegramOutlineIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import Svg from "components/svg/Svg"; -import { type SvgProps } from "../types"; -import { useThemeContext } from "context/theme-context"; - -const Icon: React.FC = ({ color = "black", ...props }) => { - const { theme } = useThemeContext(); - return ( - - - - - ); -}; - -export default Icon; diff --git a/src/typescript/frontend/src/lib/env.ts b/src/typescript/frontend/src/lib/env.ts index 8c69f0d56..3205b8af0 100644 --- a/src/typescript/frontend/src/lib/env.ts +++ b/src/typescript/frontend/src/lib/env.ts @@ -7,7 +7,6 @@ export type Links = { x: string; github: string; discord: string; - telegram: string; tos: string; }; From 8593163b93c37d3f1c78f17c491445f539c4947b Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Mon, 18 Nov 2024 22:09:20 +0100 Subject: [PATCH 50/94] [ECO-2268] Add maintenance page (#296) Co-authored-by: alnoki <43892045+alnoki@users.noreply.github.com> Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- src/typescript/frontend/src/app/error.tsx | 12 +- .../frontend/src/app/global-error.tsx | 23 +++ .../src/app/maintenance/component.tsx | 30 ++++ .../frontend/src/app/maintenance/matrix.tsx | 142 ++++++++++++++++++ .../frontend/src/app/maintenance/page.tsx | 8 + src/typescript/frontend/src/lib/server-env.ts | 2 + src/typescript/frontend/src/middleware.ts | 4 + src/typescript/frontend/src/router/routes.ts | 1 + .../sdk/src/indexer-v2/queries/app/home.ts | 4 +- .../sdk/src/indexer-v2/queries/app/market.ts | 2 +- .../sdk/src/indexer-v2/queries/utils.ts | 8 +- 11 files changed, 227 insertions(+), 9 deletions(-) create mode 100644 src/typescript/frontend/src/app/global-error.tsx create mode 100644 src/typescript/frontend/src/app/maintenance/component.tsx create mode 100644 src/typescript/frontend/src/app/maintenance/matrix.tsx create mode 100644 src/typescript/frontend/src/app/maintenance/page.tsx diff --git a/src/typescript/frontend/src/app/error.tsx b/src/typescript/frontend/src/app/error.tsx index 420698795..1b0703ab4 100644 --- a/src/typescript/frontend/src/app/error.tsx +++ b/src/typescript/frontend/src/app/error.tsx @@ -1,15 +1,17 @@ "use client"; -// Error components must be client components. This is the only component -// in this folder that is a client component. -import { ErrorBoundaryFallback } from "components/error-boundary"; +import { useEffect } from "react"; +import Maintenance from "./maintenance/component"; export default function Error({ error, - reset, }: { error: Error & { digest?: string }; reset: () => void; }) { - return ; + useEffect(() => { + console.error(error); + }, [error]); + + return ; } diff --git a/src/typescript/frontend/src/app/global-error.tsx b/src/typescript/frontend/src/app/global-error.tsx new file mode 100644 index 000000000..15ca21d72 --- /dev/null +++ b/src/typescript/frontend/src/app/global-error.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useEffect } from "react"; +import Maintenance from "./maintenance/component"; + +export default function GlobalError({ + error, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error(error); + }, [error]); + + return ( + + + + + + ); +} diff --git a/src/typescript/frontend/src/app/maintenance/component.tsx b/src/typescript/frontend/src/app/maintenance/component.tsx new file mode 100644 index 000000000..e28b9848f --- /dev/null +++ b/src/typescript/frontend/src/app/maintenance/component.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { translationFunction } from "context/language-context"; +import React from "react"; +import { useScramble } from "use-scramble"; +import { Text } from "components"; +import MatrixRain from "./matrix"; + +export default function Maintenance() { + const { t } = translationFunction(); + const { ref } = useScramble({ text: `{ ${t("maintenance")} }` }); + return ( +
+ + +
+ ); +} diff --git a/src/typescript/frontend/src/app/maintenance/matrix.tsx b/src/typescript/frontend/src/app/maintenance/matrix.tsx new file mode 100644 index 000000000..f8608f9b1 --- /dev/null +++ b/src/typescript/frontend/src/app/maintenance/matrix.tsx @@ -0,0 +1,142 @@ +/* eslint-disable jsx-a11y/anchor-is-valid */ +import useNodeDimensions from "@hooks/use-node-dimensions"; +import { type SymbolEmoji, getRandomSymbolEmoji } from "@sdk/emoji_data"; +import React, { useEffect, useRef, useState } from "react"; +import { useInterval } from "react-use"; + +// Constants +const STREAM_MUTATION_ODDS = 0.02; + +const MIN_STREAM_SIZE = 5; +const MAX_STREAM_SIZE = 10; + +const MIN_INTERVAL_DELAY = 50; +const MAX_INTERVAL_DELAY = 100; + +const MIN_DELAY_BETWEEN_STREAMS = 0; +const MAX_DELAY_BETWEEN_STREAMS = 8000; + +const getRandInRange = (min: number, max: number) => Math.floor(Math.random() * (max - min)) + min; + +const getRandChar = () => getRandomSymbolEmoji().emoji; + +const getRandStream = () => + Array.from({ length: getRandInRange(MIN_STREAM_SIZE, MAX_STREAM_SIZE) }).map((_) => + getRandChar() + ); + +const getMutatedStream = (stream: SymbolEmoji[]) => { + const newStream: SymbolEmoji[] = []; + for (let i = 1; i < stream.length; i++) { + if (Math.random() < STREAM_MUTATION_ODDS) { + newStream.push(getRandChar()); + } else { + newStream.push(stream[i]); + } + } + newStream.push(getRandChar()); + return newStream; +}; + +const RainStream = (props: { height: number }) => { + const [stream, setStream] = useState(getRandStream()); + const [topPadding, setTopPadding] = useState(stream.length * -70 - 140); + const [intervalDelay, setIntervalDelay] = useState(null); + + // Initialize intervalDelay + useEffect(() => { + setTimeout( + () => { + setIntervalDelay(getRandInRange(MIN_INTERVAL_DELAY, MAX_INTERVAL_DELAY)); + }, + getRandInRange(MIN_DELAY_BETWEEN_STREAMS, MAX_DELAY_BETWEEN_STREAMS) + ); + }, []); + + useInterval(() => { + if (!props.height) return; + + if (!intervalDelay) return; + + // If stream is off the screen, reset it after timeout + if (topPadding > props.height) { + const newStream = getRandStream(); + setStream(newStream); + setTopPadding(newStream.length * -70 - 140); + setIntervalDelay(null); + setTimeout( + () => setIntervalDelay(getRandInRange(MIN_INTERVAL_DELAY, MAX_INTERVAL_DELAY)), + getRandInRange(MIN_DELAY_BETWEEN_STREAMS, MAX_DELAY_BETWEEN_STREAMS) + ); + } else { + setTopPadding(topPadding + 70); + } + // setStream(stream => [...stream.slice(1, stream.length), getRandChar()]); + setStream(getMutatedStream); + }, intervalDelay); + + return ( +
+ {stream.map((char, index) => ( + + {char} + + ))} +
+ ); +}; + +const MatrixRain = () => { + const containerRef = useRef(null); + const containerSize = useNodeDimensions(containerRef); + + const streamCount = containerSize ? Math.floor((containerSize.width ?? 0) / 50) : 0; + + return ( +
+ {Array.from({ length: streamCount }).map((_, index) => ( + + ))} +
+ ); +}; + +export default MatrixRain; diff --git a/src/typescript/frontend/src/app/maintenance/page.tsx b/src/typescript/frontend/src/app/maintenance/page.tsx new file mode 100644 index 000000000..953e0f53d --- /dev/null +++ b/src/typescript/frontend/src/app/maintenance/page.tsx @@ -0,0 +1,8 @@ +"use client"; + +import React from "react"; +import Maintenance from "./component"; + +export default function MaintenancePage() { + return ; +} diff --git a/src/typescript/frontend/src/lib/server-env.ts b/src/typescript/frontend/src/lib/server-env.ts index 451be828f..eed62464c 100644 --- a/src/typescript/frontend/src/lib/server-env.ts +++ b/src/typescript/frontend/src/lib/server-env.ts @@ -52,3 +52,5 @@ if ( `APTOS_NETWORK is ${APTOS_NETWORK} but the indexer processor url is set to ${EMOJICOIN_INDEXER_URL}` ); } + +export const MAINTENANCE_MODE: boolean = process.env.MAINTENANCE_MODE === "true"; diff --git a/src/typescript/frontend/src/middleware.ts b/src/typescript/frontend/src/middleware.ts index 11fba00f5..e28246ce1 100644 --- a/src/typescript/frontend/src/middleware.ts +++ b/src/typescript/frontend/src/middleware.ts @@ -3,6 +3,7 @@ import { COOKIE_FOR_HASHED_ADDRESS, } from "components/pages/verify/session-info"; import { authenticate } from "components/pages/verify/verify"; +import { MAINTENANCE_MODE } from "lib/server-env"; import { IS_ALLOWLIST_ENABLED } from "lib/env"; import { NextResponse, type NextRequest } from "next/server"; import { ROUTES } from "router/routes"; @@ -10,6 +11,9 @@ import { normalizePossibleMarketPath } from "utils/pathname-helpers"; export default async function middleware(request: NextRequest) { const pathname = new URL(request.url).pathname; + if (MAINTENANCE_MODE && pathname !== "/maintenance") { + return NextResponse.redirect(new URL(ROUTES.maintenance, request.url)); + } if ( pathname === "/social-preview.png" || pathname === "/webclip.png" || diff --git a/src/typescript/frontend/src/router/routes.ts b/src/typescript/frontend/src/router/routes.ts index 52e88a4f1..1a29cb613 100644 --- a/src/typescript/frontend/src/router/routes.ts +++ b/src/typescript/frontend/src/router/routes.ts @@ -8,4 +8,5 @@ export const ROUTES = { verify: "/verify", docs: "https://docs.emojicoin.fun/category/--start-here", notFound: "/not-found", + maintenance: "/maintenance", } as const; diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/home.ts b/src/typescript/sdk/src/indexer-v2/queries/app/home.ts index 2f7a90fab..227db8ef5 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/app/home.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/app/home.ts @@ -79,8 +79,8 @@ export const fetchNumRegisteredMarkets = async () => { try { latestVersion = await getLatestProcessedEmojicoinVersion(); } catch (e) { - console.error("Couldn't get the latest processed version."); - return 0; + console.error("Couldn't get the latest processed version.", e); + throw e; } try { const numRegisteredMarkets = await RegistryView.view({ diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/market.ts b/src/typescript/sdk/src/indexer-v2/queries/app/market.ts index 7060e8634..2afd7c107 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/app/market.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/app/market.ts @@ -66,7 +66,7 @@ const selectMarketState = ({ searchEmojis }: { searchEmojis: SymbolEmoji[] }) => .select("*") .eq("symbol_emojis", toQueryArray(searchEmojis)) .limit(1) - .single(); + .maybeSingle(); const selectMarketRegistration = ({ marketID }: { marketID: AnyNumberString }) => postgrest diff --git a/src/typescript/sdk/src/indexer-v2/queries/utils.ts b/src/typescript/sdk/src/indexer-v2/queries/utils.ts index e9200c6ba..775f0c019 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/utils.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/utils.ts @@ -145,6 +145,12 @@ export function queryHelperSingle< } const res = await innerQuery; + + if (res.error) { + console.error(res.error); + throw new Error(JSON.stringify(res.error)); + } + const row = extractRow(res); return row ? convert(row) : null; }; @@ -181,11 +187,11 @@ export function queryHelperWithCount< try { const res = await innerQuery; - const rows = extractRows(res); if (res.error) { console.error("[Failed row conversion]:\n"); throw new Error(JSON.stringify(res)); } + const rows = extractRows(res); return { rows: rows.map(convert), count: res.count }; } catch (e) { console.error(e); From c8311c014baa3753ac2144b764c77374b241bb2a Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Mon, 18 Nov 2024 22:17:25 +0100 Subject: [PATCH 51/94] [ECO-2427] Cache num of markets query (#367) --- src/typescript/frontend/src/app/home/page.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/typescript/frontend/src/app/home/page.tsx b/src/typescript/frontend/src/app/home/page.tsx index f59510576..02a45349a 100644 --- a/src/typescript/frontend/src/app/home/page.tsx +++ b/src/typescript/frontend/src/app/home/page.tsx @@ -11,9 +11,16 @@ import { symbolBytesToEmojis } from "@sdk/emoji_data"; import { MARKETS_PER_PAGE } from "lib/queries/sorting/const"; import { ORDER_BY } from "@sdk/queries"; import { SortMarketsBy } from "@sdk/indexer-v2/types/common"; +import { unstable_cache } from "next/cache"; export const revalidate = 2; +const getCachedNumMarketsFromAptosNode = unstable_cache( + fetchNumRegisteredMarkets, + ["num-registered-markets"], + { revalidate: 10 } +); + export default async function Home({ searchParams }: HomePageParams) { const { page, sortBy, orderBy, q } = toHomePageParamsWithDefault(searchParams); const searchEmojis = q ? symbolBytesToEmojis(q).emojis.map((e) => e.emoji) : undefined; @@ -43,7 +50,7 @@ export default async function Home({ searchParams }: HomePageParams) { searchEmojis, pageSize: MARKETS_PER_PAGE, }); - numMarketsPromise = fetchNumRegisteredMarkets(); + numMarketsPromise = getCachedNumMarketsFromAptosNode(); } let featuredPromise: ReturnType; From 2ad51f65ea24825c4db471c30d6cea4160eeeb5e Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Mon, 18 Nov 2024 22:54:12 +0100 Subject: [PATCH 52/94] [ECO-2435] Add pre launch page (#370) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- .../frontend/src/app/launching/page.tsx | 37 +++++++++++++++++++ src/typescript/frontend/src/lib/server-env.ts | 1 + src/typescript/frontend/src/middleware.ts | 8 +++- src/typescript/frontend/src/router/routes.ts | 1 + 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/typescript/frontend/src/app/launching/page.tsx diff --git a/src/typescript/frontend/src/app/launching/page.tsx b/src/typescript/frontend/src/app/launching/page.tsx new file mode 100644 index 000000000..bc893b15d --- /dev/null +++ b/src/typescript/frontend/src/app/launching/page.tsx @@ -0,0 +1,37 @@ +"use client"; + +import React, { useMemo } from "react"; +import MatrixRain from "../maintenance/matrix"; +import { useScramble } from "use-scramble"; +import { Text } from "components"; +import { getRandomChatEmoji } from "@sdk/emoji_data"; + +export default function LaunchingPage() { + const catchPhrase = useMemo(() => { + const phrases = ["launching soon", "get ready"]; + const index = Math.floor(Math.random() * phrases.length); + const emoji = getRandomChatEmoji().emoji; + return `${emoji} ${phrases[index]} ${emoji}`; + }, []); + const { ref } = useScramble({ text: catchPhrase }); + return ( +
+ + +
+ ); +} diff --git a/src/typescript/frontend/src/lib/server-env.ts b/src/typescript/frontend/src/lib/server-env.ts index eed62464c..38ea2332e 100644 --- a/src/typescript/frontend/src/lib/server-env.ts +++ b/src/typescript/frontend/src/lib/server-env.ts @@ -42,6 +42,7 @@ export const ALLOWLISTER3K_URL: string | undefined = process.env.ALLOWLISTER3K_U export const GALXE_CAMPAIGN_ID: string | undefined = process.env.GALXE_CAMPAIGN_ID; export const REVALIDATION_TIME: number = Number(process.env.REVALIDATION_TIME); export const VPNAPI_IO_API_KEY: string = process.env.VPNAPI_IO_API_KEY!; +export const PRE_LAUNCH_TEASER: boolean = process.env.PRE_LAUNCH_TEASER === "true"; if ( APTOS_NETWORK === Network.LOCAL && diff --git a/src/typescript/frontend/src/middleware.ts b/src/typescript/frontend/src/middleware.ts index e28246ce1..b324fcfcb 100644 --- a/src/typescript/frontend/src/middleware.ts +++ b/src/typescript/frontend/src/middleware.ts @@ -3,7 +3,7 @@ import { COOKIE_FOR_HASHED_ADDRESS, } from "components/pages/verify/session-info"; import { authenticate } from "components/pages/verify/verify"; -import { MAINTENANCE_MODE } from "lib/server-env"; +import { MAINTENANCE_MODE, PRE_LAUNCH_TEASER } from "lib/server-env"; import { IS_ALLOWLIST_ENABLED } from "lib/env"; import { NextResponse, type NextRequest } from "next/server"; import { ROUTES } from "router/routes"; @@ -11,6 +11,12 @@ import { normalizePossibleMarketPath } from "utils/pathname-helpers"; export default async function middleware(request: NextRequest) { const pathname = new URL(request.url).pathname; + if (pathname === "/launching") { + return NextResponse.next(); + } + if (PRE_LAUNCH_TEASER && pathname !== "/launching") { + return NextResponse.redirect(new URL(ROUTES.launching, request.url)); + } if (MAINTENANCE_MODE && pathname !== "/maintenance") { return NextResponse.redirect(new URL(ROUTES.maintenance, request.url)); } diff --git a/src/typescript/frontend/src/router/routes.ts b/src/typescript/frontend/src/router/routes.ts index 1a29cb613..eb96d0cda 100644 --- a/src/typescript/frontend/src/router/routes.ts +++ b/src/typescript/frontend/src/router/routes.ts @@ -9,4 +9,5 @@ export const ROUTES = { docs: "https://docs.emojicoin.fun/category/--start-here", notFound: "/not-found", maintenance: "/maintenance", + launching: "/launching", } as const; From a74ccf134ffd3b54e715adc45e3b5d63300bbc21 Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:24:01 -0800 Subject: [PATCH 53/94] [ECO-23439] Add `--move-2` flag to rewards publish command (#373) --- src/move/rewards/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/move/rewards/README.md b/src/move/rewards/README.md index 95d7de08e..7b78b4b68 100644 --- a/src/move/rewards/README.md +++ b/src/move/rewards/README.md @@ -35,6 +35,7 @@ NAMED_ADDRESSES=$( ) aptos move publish \ --assume-yes \ + --move-2 \ --named-addresses $NAMED_ADDRESSES \ --profile $PROFILE ``` From 5192170cfde7107cdd5a542eb22aff86249d3c9c Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:42:53 -0800 Subject: [PATCH 54/94] [ECO-2432] Misc deploy file updates (#369) --- src/cloud-formation/README.md | 17 ++++++++++------- src/cloud-formation/deploy-indexer-main.yaml | 4 ++-- .../deploy-indexer-production.yaml | 6 +++--- src/cloud-formation/indexer.cfn.yaml | 1 + 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/cloud-formation/README.md b/src/cloud-formation/README.md index 477d47b8e..805f6f962 100644 --- a/src/cloud-formation/README.md +++ b/src/cloud-formation/README.md @@ -73,7 +73,9 @@ indexer deployments. A plaintext API key for the [transaction stream service endpoint] you are connecting to, for example an - [Aptos Labs transaction stream service API key]. + [Aptos Labs transaction stream service API key]. Note that you'll need to + prepend `https://` as applicable if it is a public endpoint, for example + `https://grpc.devnet.aptoslabs.com:443` @@ -96,7 +98,7 @@ indexer deployments. Description - `/emojicoin/grpc-data-service-url/` + `/emojicoin/grpc-data-service-url/` @@ -120,14 +122,14 @@ indexer deployments. - `/emojicoin/minimum-starting-version/` + `/emojicoin/minimum-starting-version/` A transaction version number prior to the version in which the target Move package was published. - `/emojicoin/package-address/` + `/emojicoin/package-address/` The address of the Move package you want to index. @@ -135,9 +137,10 @@ indexer deployments. - > Substitute either `mainnet` or `testnet` for `` depending - > on the network you want to index (create a parameter for each network if - > you want to run deployments for both). + > Substitute either `mainnet`, `testnet`, or `devnet` for + > `` depending on the network you want to index + > (create a parameter for each network if you want to run deployments for + > all). 1. Create the following [IAM roles]: diff --git a/src/cloud-formation/deploy-indexer-main.yaml b/src/cloud-formation/deploy-indexer-main.yaml index a846b99fd..eebd44b07 100644 --- a/src/cloud-formation/deploy-indexer-main.yaml +++ b/src/cloud-formation/deploy-indexer-main.yaml @@ -1,6 +1,6 @@ --- parameters: - BrokerImageVersion: '1.0.0' + BrokerImageVersion: '1.0.1' DeployAlb: 'true' DeployAlbDnsRecord: 'true' DeployBastionHost: 'true' @@ -20,7 +20,7 @@ parameters: EnableWafRulesWebSocket: 'false' Environment: 'main' Network: 'testnet' - ProcessorImageVersion: '1.0.0' + ProcessorImageVersion: '1.0.1' VpcStackName: 'emoji-vpc' tags: null template-file-path: 'src/cloud-formation/indexer.cfn.yaml' diff --git a/src/cloud-formation/deploy-indexer-production.yaml b/src/cloud-formation/deploy-indexer-production.yaml index af844b796..4de5c7651 100644 --- a/src/cloud-formation/deploy-indexer-production.yaml +++ b/src/cloud-formation/deploy-indexer-production.yaml @@ -1,6 +1,6 @@ --- parameters: - BrokerImageVersion: '1.0.0' + BrokerImageVersion: '1.0.1' DeployAlb: 'true' DeployAlbDnsRecord: 'true' DeployBastionHost: 'true' @@ -19,8 +19,8 @@ parameters: EnableWafRulesRestApi: 'false' EnableWafRulesWebSocket: 'false' Environment: 'production' - Network: 'testnet' - ProcessorImageVersion: '1.0.0' + Network: 'devnet' + ProcessorImageVersion: '1.0.1' VpcStackName: 'emoji-vpc' tags: null template-file-path: 'src/cloud-formation/indexer.cfn.yaml' diff --git a/src/cloud-formation/indexer.cfn.yaml b/src/cloud-formation/indexer.cfn.yaml index a35655785..1e1df2570 100644 --- a/src/cloud-formation/indexer.cfn.yaml +++ b/src/cloud-formation/indexer.cfn.yaml @@ -206,6 +206,7 @@ Parameters: AllowedValues: - 'mainnet' - 'testnet' + - 'devnet' Type: 'String' PostgrestImageVersion: Default: 'v12.2.3' From f2e6906cd91afb2c6a91b147d87bbd214bad7ef5 Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:24:31 -0800 Subject: [PATCH 55/94] [ECO-2440] Add typing, quoting to fund command (#375) --- src/move/rewards/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/move/rewards/README.md b/src/move/rewards/README.md index 7b78b4b68..788c741ef 100644 --- a/src/move/rewards/README.md +++ b/src/move/rewards/README.md @@ -45,7 +45,7 @@ aptos move publish \ ```sh REWARDS=0xaaa... PROFILE=my-profile -N_CLAIM_LINK_REDEMPTIONS_TO_FUND=10 +N_CLAIM_LINK_REDEMPTIONS_TO_FUND="u64:10" N_REWARDS_TO_FUND_PER_TIER="u64:[1500,500,200,50,5,1]" ``` From 0719b45ac336092604549e910cfa0c0e9d1f52da Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 18 Nov 2024 18:59:22 -0800 Subject: [PATCH 56/94] [ECO-2436] Add Aptos API keys to the `Aptos` client anywhere we use it (#374) --- src/docker/compose.yaml | 5 +++ src/docker/example.local.env | 7 ++++ src/docker/example.testnet.env | 7 ++++ src/docker/frontend/Dockerfile | 18 ++++++-- src/typescript/frontend/.eslintignore | 1 + src/typescript/frontend/src/app/test/route.ts | 3 ++ .../frontend/src/app/verify_api_keys/page.tsx | 25 +++++++++++ .../src/components/charts/PrivateChart.tsx | 5 +-- .../frontend/src/context/providers.tsx | 6 ++- src/typescript/frontend/src/lib/env.ts | 23 +++++------ src/typescript/frontend/src/lib/server-env.ts | 3 +- .../frontend/src/lib/utils/aptos-client.ts | 8 +++- src/typescript/package.json | 7 ++-- .../sdk/src/client/emojicoin-client.ts | 12 ++++-- src/typescript/sdk/src/const.ts | 41 ++++++++++++++++++- .../src/emojicoin_dot_fun/aptos-framework.ts | 9 ++-- .../emojicoin_dot_fun/emojicoin-dot-fun.ts | 19 +++++---- src/typescript/sdk/src/utils/aptos-client.ts | 21 +++++----- .../tests/e2e/queries/client/submit.test.ts | 26 +++++++++++- src/typescript/turbo.json | 6 +++ 20 files changed, 194 insertions(+), 58 deletions(-) create mode 100644 src/typescript/frontend/src/app/verify_api_keys/page.tsx diff --git a/src/docker/compose.yaml b/src/docker/compose.yaml index b16ae890f..f044a5c82 100644 --- a/src/docker/compose.yaml +++ b/src/docker/compose.yaml @@ -129,6 +129,11 @@ services: NEXT_PUBLIC_BROKER_URL: 'ws://host.docker.internal:${BROKER_PORT}' NEXT_PUBLIC_REWARDS_MODULE_ADDRESS: '${EMOJICOIN_REWARDS_MODULE_ADDRESS}' REVALIDATION_TIME: '${REVALIDATION_TIME}' + NEXT_PUBLIC_LOCAL_APTOS_API_KEY: '${FRONTEND_LOCAL_APTOS_API_KEY}' + NEXT_PUBLIC_CUSTOM_APTOS_API_KEY: '${FRONTEND_CUSTOM_APTOS_API_KEY}' + NEXT_PUBLIC_DEVNET_APTOS_API_KEY: '${FRONTEND_DEVNET_APTOS_API_KEY}' + NEXT_PUBLIC_TESTNET_APTOS_API_KEY: '${FRONTEND_TESTNET_APTOS_API_KEY}' + NEXT_PUBLIC_MAINNET_APTOS_API_KEY: '${FRONTEND_MAINNET_APTOS_API_KEY}' healthcheck: test: 'curl -f http://localhost:3001/ || exit 1' interval: '30s' diff --git a/src/docker/example.local.env b/src/docker/example.local.env index 268f5a1f2..4fe34dba4 100644 --- a/src/docker/example.local.env +++ b/src/docker/example.local.env @@ -77,3 +77,10 @@ FEE_RATE_BPS="100" # Secret hash seed. HASH_SEED="some random string that is not public" + +# Set the API keys for the various frontend networks. It is okay if these are exposed publicly. +FRONTEND_LOCAL_APTOS_API_KEY="" +FRONTEND_CUSTOM_APTOS_API_KEY="" +FRONTEND_DEVNET_APTOS_API_KEY="AG-MSPVZN9BNPNB6KWSPZN7GHSETJU2TFLHJ" # cspell:disable-line +FRONTEND_TESTNET_APTOS_API_KEY="AG-FRGKQEVCNY5PDJKZBAVTVCPGEQ6YFUBBX" # cspell:disable-line +FRONTEND_MAINNET_APTOS_API_KEY="AG-BAGXRD2QME5WFTBZVAR4BPA7M5EGTBRHQ" # cspell:disable-line diff --git a/src/docker/example.testnet.env b/src/docker/example.testnet.env index 39df3c32c..ea38fe114 100644 --- a/src/docker/example.testnet.env +++ b/src/docker/example.testnet.env @@ -64,3 +64,10 @@ FEE_RATE_BPS="100" # Secret hash seed. HASH_SEED="some random string that is not public" + +# Set the API keys for the various frontend networks. It is okay if these are exposed publicly. +FRONTEND_LOCAL_APTOS_API_KEY="" +FRONTEND_CUSTOM_APTOS_API_KEY="" +FRONTEND_DEVNET_APTOS_API_KEY="AG-MSPVZN9BNPNB6KWSPZN7GHSETJU2TFLHJ" # cspell:disable-line +FRONTEND_TESTNET_APTOS_API_KEY="AG-FRGKQEVCNY5PDJKZBAVTVCPGEQ6YFUBBX" # cspell:disable-line +FRONTEND_MAINNET_APTOS_API_KEY="AG-BAGXRD2QME5WFTBZVAR4BPA7M5EGTBRHQ" # cspell:disable-line diff --git a/src/docker/frontend/Dockerfile b/src/docker/frontend/Dockerfile index 908e99374..f1f03d67c 100644 --- a/src/docker/frontend/Dockerfile +++ b/src/docker/frontend/Dockerfile @@ -19,7 +19,12 @@ ARG HASH_SEED \ NEXT_PUBLIC_REWARDS_MODULE_ADDRESS \ NEXT_PUBLIC_BROKER_URL \ REVALIDATION_TIME \ - EMOJICOIN_INDEXER_URL + EMOJICOIN_INDEXER_URL \ + NEXT_PUBLIC_LOCAL_APTOS_API_KEY \ + NEXT_PUBLIC_CUSTOM_APTOS_API_KEY \ + NEXT_PUBLIC_DEVNET_APTOS_API_KEY \ + NEXT_PUBLIC_TESTNET_APTOS_API_KEY \ + NEXT_PUBLIC_MAINNET_APTOS_API_KEY ENV HASH_SEED=$HASH_SEED \ NEXT_PUBLIC_APTOS_NETWORK=$NEXT_PUBLIC_APTOS_NETWORK \ NEXT_PUBLIC_INTEGRATOR_ADDRESS=$NEXT_PUBLIC_INTEGRATOR_ADDRESS \ @@ -29,8 +34,13 @@ ENV HASH_SEED=$HASH_SEED \ NEXT_PUBLIC_REWARDS_MODULE_ADDRESS=$NEXT_PUBLIC_REWARDS_MODULE_ADDRESS \ NEXT_PUBLIC_BROKER_URL=$NEXT_PUBLIC_BROKER_URL \ REVALIDATION_TIME=$REVALIDATION_TIME \ - EMOJICOIN_INDEXER_URL=$EMOJICOIN_INDEXER_URL - -RUN ["bash", "-c", "pnpm install && pnpm run build:no-checks"] + EMOJICOIN_INDEXER_URL=$EMOJICOIN_INDEXER_URL \ + NEXT_PUBLIC_LOCAL_APTOS_API_KEY=$FRONTEND_LOCAL_APTOS_API_KEY \ + NEXT_PUBLIC_CUSTOM_APTOS_API_KEY=$FRONTEND_CUSTOM_APTOS_API_KEY \ + NEXT_PUBLIC_DEVNET_APTOS_API_KEY=$FRONTEND_DEVNET_APTOS_API_KEY \ + NEXT_PUBLIC_TESTNET_APTOS_API_KEY=$FRONTEND_TESTNET_APTOS_API_KEY \ + NEXT_PUBLIC_MAINNET_APTOS_API_KEY=$FRONTEND_MAINNET_APTOS_API_KEY + +RUN ["bash", "-c", "pnpm install && pnpm run build:test"] CMD ["bash", "-c", "pnpm run start -- -H 0.0.0.0"] diff --git a/src/typescript/frontend/.eslintignore b/src/typescript/frontend/.eslintignore index 429512ba7..655d36222 100644 --- a/src/typescript/frontend/.eslintignore +++ b/src/typescript/frontend/.eslintignore @@ -3,3 +3,4 @@ dist/ .next/ public/static/ tests/e2e/global.*.ts +playwright-report/ diff --git a/src/typescript/frontend/src/app/test/route.ts b/src/typescript/frontend/src/app/test/route.ts index afee2db37..b2681c6c3 100644 --- a/src/typescript/frontend/src/app/test/route.ts +++ b/src/typescript/frontend/src/app/test/route.ts @@ -5,6 +5,9 @@ export const revalidate = 2; export const fetchCache = "default-cache"; export async function GET() { + if (process.env.NODE_ENV !== "test") { + return new NextResponse("-1"); + } const aptos = getAptos(); try { const version = await aptos.getLedgerInfo().then((res) => res.ledger_version); diff --git a/src/typescript/frontend/src/app/verify_api_keys/page.tsx b/src/typescript/frontend/src/app/verify_api_keys/page.tsx new file mode 100644 index 000000000..040d57afb --- /dev/null +++ b/src/typescript/frontend/src/app/verify_api_keys/page.tsx @@ -0,0 +1,25 @@ +import { AccountAddress } from "@aptos-labs/ts-sdk"; +import { APTOS_API_KEY } from "@sdk/const"; +import { getAptosClient } from "@sdk/utils/aptos-client"; + +export const dynamic = "force-static"; +export const revalidate = 600; +export const runtime = "nodejs"; + +const VerifyApiKeys = async () => { + const { aptos } = getAptosClient(); + const accountAddress = AccountAddress.ONE; + let balance = 0; + try { + balance = await aptos.account.getAccountAPTAmount({ accountAddress }); + } catch (e) { + const msg = `\n\tLikely an invalid API key. APTOS_API_KEY: ${APTOS_API_KEY}`; + throw new Error(`Couldn't fetch ${accountAddress}'s balance. ${msg}`); + } + + return ( +
{`Balance: ${balance}`}
+ ); +}; + +export default VerifyApiKeys; diff --git a/src/typescript/frontend/src/components/charts/PrivateChart.tsx b/src/typescript/frontend/src/components/charts/PrivateChart.tsx index dc0cb2552..ff10a9499 100644 --- a/src/typescript/frontend/src/components/charts/PrivateChart.tsx +++ b/src/typescript/frontend/src/components/charts/PrivateChart.tsx @@ -28,11 +28,10 @@ import path from "path"; import { emojisToName } from "lib/utils/emojis-to-name-or-symbol"; import { useEventStore } from "context/event-store-context"; import { getPeriodStartTimeFromTime } from "@sdk/utils"; -import { getAptosConfig } from "lib/utils/aptos-client"; +import { getAptos } from "lib/utils/aptos-client"; import { getSymbolEmojisInString, symbolToEmojis, toMarketEmojiData } from "@sdk/emoji_data"; import { type PeriodicStateEventModel, type MarketMetadataModel } from "@sdk/indexer-v2/types"; import { getMarketResource } from "@sdk/markets"; -import { Aptos } from "@aptos-labs/ts-sdk"; import { periodEnumToRawDuration, Trigger } from "@sdk/const"; import { type LatestBar, @@ -184,7 +183,7 @@ export const Chart = (props: ChartContainerProps) => { // Also, we specifically call this client-side because the server will get rate-limited if we call the // fullnode from the server for each client. const marketResource = await getMarketResource({ - aptos: new Aptos(getAptosConfig()), + aptos: getAptos(), marketAddress: props.marketAddress, }); diff --git a/src/typescript/frontend/src/context/providers.tsx b/src/typescript/frontend/src/context/providers.tsx index 4de51669c..cccbec7e7 100644 --- a/src/typescript/frontend/src/context/providers.tsx +++ b/src/typescript/frontend/src/context/providers.tsx @@ -28,6 +28,7 @@ import { RiseWallet } from "@rise-wallet/wallet-adapter"; import { MartianWallet } from "@martianwallet/aptos-wallet-adapter"; import { EmojiPickerProvider } from "./emoji-picker-context/EmojiPickerContextProvider"; import { isMobile, isTablet } from "react-device-detect"; +import { APTOS_API_KEY } from "@sdk/const"; enableMapSet(); @@ -50,7 +51,10 @@ const ThemedApp: React.FC<{ children: React.ReactNode }> = ({ children }) => { diff --git a/src/typescript/frontend/src/lib/env.ts b/src/typescript/frontend/src/lib/env.ts index 3205b8af0..0e0d4efa9 100644 --- a/src/typescript/frontend/src/lib/env.ts +++ b/src/typescript/frontend/src/lib/env.ts @@ -1,7 +1,7 @@ -import { type Network } from "@aptos-labs/wallet-adapter-react"; import packageInfo from "../../package.json"; import { parse } from "semver"; import { type AccountAddressString } from "@sdk/emojicoin_dot_fun"; +import { type Network } from "@aptos-labs/ts-sdk"; export type Links = { x: string; @@ -10,7 +10,15 @@ export type Links = { tos: string; }; -let APTOS_NETWORK: Network; +const network = process.env.NEXT_PUBLIC_APTOS_NETWORK; +const APTOS_NETWORK = network as Network; +// NOTE: We must check it this way instead of with `NetworkToNetworkName[APTOS_NETWORK]` because +// otherwise the @aptos-labs/ts-sdk package is included in the middleware.ts function and the edge +// runtime won't build properly. +if (!["local", "devnet", "testnet", "mainnet", "custom"].includes(APTOS_NETWORK)) { + throw new Error(`Invalid network: ${network}`); +} + let INTEGRATOR_ADDRESS: AccountAddressString; let INTEGRATOR_FEE_RATE_BPS: number; let BROKER_URL: string; @@ -22,17 +30,6 @@ export const LINKS: Links | undefined = const IS_ALLOWLIST_ENABLED: boolean = process.env.NEXT_PUBLIC_IS_ALLOWLIST_ENABLED === "true"; -if (process.env.NEXT_PUBLIC_APTOS_NETWORK) { - const network = process.env.NEXT_PUBLIC_APTOS_NETWORK; - if (["mainnet", "testnet", "devnet", "local", "custom", "docker"].includes(network)) { - APTOS_NETWORK = network as Network; - } else { - throw new Error(`Invalid network: ${network}`); - } -} else { - throw new Error("Environment variable NEXT_PUBLIC_APTOS_NETWORK is undefined."); -} - if (process.env.NEXT_PUBLIC_INTEGRATOR_ADDRESS) { INTEGRATOR_ADDRESS = process.env.NEXT_PUBLIC_INTEGRATOR_ADDRESS as AccountAddressString; } else { diff --git a/src/typescript/frontend/src/lib/server-env.ts b/src/typescript/frontend/src/lib/server-env.ts index 38ea2332e..9d2966f02 100644 --- a/src/typescript/frontend/src/lib/server-env.ts +++ b/src/typescript/frontend/src/lib/server-env.ts @@ -1,6 +1,5 @@ import "server-only"; import { APTOS_NETWORK, IS_ALLOWLIST_ENABLED } from "./env"; -import { Network } from "@aptos-labs/ts-sdk"; import { EMOJICOIN_INDEXER_URL } from "@sdk/server/env"; if (typeof process.env.REVALIDATION_TIME === "undefined") { @@ -45,7 +44,7 @@ export const VPNAPI_IO_API_KEY: string = process.env.VPNAPI_IO_API_KEY!; export const PRE_LAUNCH_TEASER: boolean = process.env.PRE_LAUNCH_TEASER === "true"; if ( - APTOS_NETWORK === Network.LOCAL && + APTOS_NETWORK.toString() === "local" && !EMOJICOIN_INDEXER_URL.includes("localhost") && !EMOJICOIN_INDEXER_URL.includes("docker") ) { diff --git a/src/typescript/frontend/src/lib/utils/aptos-client.ts b/src/typescript/frontend/src/lib/utils/aptos-client.ts index b6f2bb710..2adbc8b45 100644 --- a/src/typescript/frontend/src/lib/utils/aptos-client.ts +++ b/src/typescript/frontend/src/lib/utils/aptos-client.ts @@ -2,13 +2,16 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import { NetworkToFaucetAPI, NetworkToIndexerAPI, NetworkToNodeAPI } from "@aptos-labs/ts-sdk"; import { Aptos, AptosConfig, NetworkToNetworkName } from "@aptos-labs/ts-sdk"; +import { APTOS_CONFIG } from "@sdk/utils/aptos-client"; import { APTOS_NETWORK } from "lib/env"; const toDockerUrl = (url: string) => url.replace("127.0.0.1", "host.docker.internal"); -// Get an Aptos config based off of the network environment variables. +// Get an Aptos config based off of the network environment variables and the default APTOS_CONFIG +// client configuration/settings. export const getAptosConfig = (): AptosConfig => { const networkString = APTOS_NETWORK; + const clientConfig = APTOS_CONFIG; if (networkString === "local" && typeof window === "undefined") { const fs = require("node:fs"); if (fs.existsSync("/.dockerenv")) { @@ -17,6 +20,7 @@ export const getAptosConfig = (): AptosConfig => { fullnode: toDockerUrl(NetworkToNodeAPI["local"]), indexer: toDockerUrl(NetworkToIndexerAPI["local"]), faucet: toDockerUrl(NetworkToFaucetAPI["local"]), + clientConfig, }); } } @@ -27,7 +31,7 @@ export const getAptosConfig = (): AptosConfig => { if (!network) { throw new Error(`Invalid network: ${networkString}`); } - return new AptosConfig({ network, fullnode, indexer, faucet }); + return new AptosConfig({ network, fullnode, indexer, faucet, clientConfig }); }; // Get an Aptos client based off of the network environment variables. diff --git a/src/typescript/package.json b/src/typescript/package.json index 4e734dfab..25d3a5191 100644 --- a/src/typescript/package.json +++ b/src/typescript/package.json @@ -17,9 +17,10 @@ "keyv": "npm:@keyvhq/core@2.1.1" }, "scripts": { - "build": "pnpm i && pnpm load-env:test -- turbo run build", - "build:debug": "pnpm i && pnpm load-env:test -- turbo run build:debug", - "build:no-checks": "pnpm i && pnpm load-env:test -- turbo run build:no-checks", + "build": "pnpm i && pnpm load-env -- turbo run build", + "build:debug": "pnpm i && pnpm load-env -- turbo run build:debug", + "build:no-checks": "pnpm i && pnpm load-env -- turbo run build:no-checks", + "build:test": "pnpm i && pnpm load-env:test -- turbo run build:no-checks", "check": "turbo run check", "clean": "turbo run clean --no-cache --force && rm -rf .turbo", "clean:full": "pnpm run clean && rm -rf node_modules && rm -rf sdk/node_modules && rm -rf frontend/node_modules", diff --git a/src/typescript/sdk/src/client/emojicoin-client.ts b/src/typescript/sdk/src/client/emojicoin-client.ts index 3f936abea..7fe497214 100644 --- a/src/typescript/sdk/src/client/emojicoin-client.ts +++ b/src/typescript/sdk/src/client/emojicoin-client.ts @@ -1,12 +1,13 @@ import { AccountAddress, + Aptos, type Account, type UserTransactionResponse, type AccountAddressInput, - type Aptos, type TypeTag, type InputGenerateTransactionOptions, type WaitForTransactionOptions, + AptosConfig, } from "@aptos-labs/ts-sdk"; import { type ChatEmoji, type SymbolEmoji } from "../emoji_data/types"; import { EmojicoinDotFun, getEvents } from "../emojicoin_dot_fun"; @@ -21,7 +22,7 @@ import { import { type Events } from "../emojicoin_dot_fun/events"; import { getEmojicoinMarketAddressAndTypeTags } from "../markets"; import { type EventsModels, getEventsAsProcessorModelsFromResponse } from "../mini-processor"; -import { getAptosClient } from "../utils/aptos-client"; +import { APTOS_CONFIG, getAptosClient } from "../utils/aptos-client"; import { toChatMessageEntryFunctionArgs } from "../emoji_data"; import customExpect from "./expect"; import { DEFAULT_REGISTER_MARKET_GAS_OPTIONS, INTEGRATOR_ADDRESS } from "../const"; @@ -145,7 +146,12 @@ export class EmojicoinClient { integratorFeeRateBPs = 0, minOutputAmount = 1n, } = args ?? {}; - this.aptos = aptos; + // Create a client that always uses the static API_KEY config options. + const hardCodedConfig = new AptosConfig({ + ...aptos.config, + clientConfig: { ...aptos.config.clientConfig, ...APTOS_CONFIG }, + }); + this.aptos = new Aptos(hardCodedConfig); this.integrator = AccountAddress.from(integrator); this.integratorFeeRateBPs = Number(integratorFeeRateBPs); this.minOutputAmount = BigInt(minOutputAmount); diff --git a/src/typescript/sdk/src/const.ts b/src/typescript/sdk/src/const.ts index d936c75ac..0c03b1f9e 100644 --- a/src/typescript/sdk/src/const.ts +++ b/src/typescript/sdk/src/const.ts @@ -1,4 +1,10 @@ -import { AccountAddress, APTOS_COIN, parseTypeTag } from "@aptos-labs/ts-sdk"; +import { + AccountAddress, + APTOS_COIN, + Network, + NetworkToNetworkName, + parseTypeTag, +} from "@aptos-labs/ts-sdk"; import Big from "big.js"; import { type ValueOf } from "./utils/utility-types"; import { type DatabaseStructType } from "./indexer-v2/types/json-types"; @@ -8,13 +14,15 @@ if ( !process.env.NEXT_PUBLIC_MODULE_ADDRESS || !process.env.NEXT_PUBLIC_REWARDS_MODULE_ADDRESS || !process.env.NEXT_PUBLIC_INTEGRATOR_ADDRESS || - !process.env.NEXT_PUBLIC_INTEGRATOR_FEE_RATE_BPS + !process.env.NEXT_PUBLIC_INTEGRATOR_FEE_RATE_BPS || + !process.env.NEXT_PUBLIC_APTOS_NETWORK ) { const missing = [ ["NEXT_PUBLIC_MODULE_ADDRESS", process.env.NEXT_PUBLIC_MODULE_ADDRESS], ["NEXT_PUBLIC_REWARDS_MODULE_ADDRESS", process.env.NEXT_PUBLIC_REWARDS_MODULE_ADDRESS], ["NEXT_PUBLIC_INTEGRATOR_ADDRESS", process.env.NEXT_PUBLIC_INTEGRATOR_ADDRESS], ["NEXT_PUBLIC_INTEGRATOR_FEE_RATE_BPS", process.env.NEXT_PUBLIC_INTEGRATOR_FEE_RATE_BPS], + ["NEXT_PUBLIC_APTOS_NETWORK", process.env.NEXT_PUBLIC_APTOS_NETWORK], ].filter(([_, value]) => !value); missing.forEach(([key, _]) => { console.error(`Missing environment variables ${key}`); @@ -26,6 +34,35 @@ if ( ); } +const network = process.env.NEXT_PUBLIC_APTOS_NETWORK; +export const APTOS_NETWORK = NetworkToNetworkName[network]; +if (!APTOS_NETWORK) { + throw new Error(`Invalid network: ${network}`); +} + +const allAPIKeys: Record = { + [Network.LOCAL]: process.env.NEXT_PUBLIC_LOCAL_APTOS_API_KEY, + [Network.CUSTOM]: process.env.NEXT_PUBLIC_CUSTOM_APTOS_API_KEY, + [Network.DEVNET]: process.env.NEXT_PUBLIC_DEVNET_APTOS_API_KEY, + [Network.TESTNET]: process.env.NEXT_PUBLIC_TESTNET_APTOS_API_KEY, + [Network.MAINNET]: process.env.NEXT_PUBLIC_MAINNET_APTOS_API_KEY, +}; + +const apiKey = allAPIKeys[APTOS_NETWORK]; +if (typeof apiKey === "undefined") { + // Do nothing if we're on a local network, because we don't need an API key for it. + if (APTOS_NETWORK !== "local") { + if (APTOS_NETWORK === "custom") { + console.warn(`No API key set. Ignoring because we're on the \`${APTOS_NETWORK}\` network.`); + } else { + throw new Error(`Invalid API key set for the network: ${APTOS_NETWORK}: ${apiKey}`); + } + } +} +// Select the API key from the list of env API keys. This means we don't have to change the env +// var for API keys when changing environments- we just need to provide them all every time, which +// is much simpler. +export const APTOS_API_KEY = apiKey; export const MODULE_ADDRESS = (() => AccountAddress.from(process.env.NEXT_PUBLIC_MODULE_ADDRESS))(); export const REWARDS_MODULE_ADDRESS = (() => AccountAddress.from(process.env.NEXT_PUBLIC_REWARDS_MODULE_ADDRESS))(); diff --git a/src/typescript/sdk/src/emojicoin_dot_fun/aptos-framework.ts b/src/typescript/sdk/src/emojicoin_dot_fun/aptos-framework.ts index a0a8f21be..61e246e5b 100644 --- a/src/typescript/sdk/src/emojicoin_dot_fun/aptos-framework.ts +++ b/src/typescript/sdk/src/emojicoin_dot_fun/aptos-framework.ts @@ -8,7 +8,7 @@ import { type AptosConfig, type InputGenerateTransactionOptions, buildTransaction, - Aptos, + type Aptos, type Account, type WaitForTransactionOptions, type UserTransactionResponse, @@ -22,6 +22,7 @@ import { ViewFunctionPayloadBuilder, } from "./payload-builders"; import { type TypeTagInput } from "."; +import { getAptosClient } from "../utils/aptos-client"; export type MintPayloadMoveArguments = { dstAddr: AccountAddress; @@ -89,7 +90,7 @@ export class Mint extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const aptos = new Aptos(aptosConfig); + const { aptos } = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -189,7 +190,7 @@ export class BatchTransferCoins extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const aptos = new Aptos(aptosConfig); + const { aptos } = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -290,7 +291,7 @@ export class TransferCoins extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const aptos = new Aptos(aptosConfig); + const { aptos } = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } diff --git a/src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts b/src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts index 20f136eb6..69ad70d93 100644 --- a/src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts +++ b/src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts @@ -7,7 +7,7 @@ import { U8, Bool, type Account, - Aptos, + type Aptos, type AptosConfig, type AccountAddressInput, type HexInput, @@ -35,6 +35,7 @@ import { } from "./payload-builders"; import { MODULE_ADDRESS, REWARDS_MODULE_ADDRESS } from "../const"; import type JsonTypes from "../types/json-types"; +import { getAptosClient } from "../utils/aptos-client"; export type ChatPayloadMoveArguments = { marketAddress: AccountAddress; @@ -112,7 +113,7 @@ export class Chat extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const aptos = new Aptos(aptosConfig); + const { aptos } = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -219,7 +220,7 @@ export class ProvideLiquidity extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const aptos = new Aptos(aptosConfig); + const { aptos } = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -307,7 +308,7 @@ export class RegisterMarket extends EntryFunctionPayloadBuilder { }): Promise<{ data: { amount: number; unitPrice: number }; error: boolean }> { const { aptosConfig } = args; - const aptos = new Aptos(aptosConfig); + const { aptos } = getAptosClient(aptosConfig); const rawTransaction = await this.builder({ ...args, integrator: AccountAddress.ONE, @@ -347,7 +348,7 @@ export class RegisterMarket extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const aptos = new Aptos(aptosConfig); + const { aptos } = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -452,7 +453,7 @@ export class RemoveLiquidity extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const aptos = new Aptos(aptosConfig); + const { aptos } = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -584,7 +585,7 @@ export class Swap extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const aptos = new Aptos(aptosConfig); + const { aptos } = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -633,7 +634,7 @@ export class Swap extends EntryFunctionPayloadBuilder { }): Promise { const { aptosConfig } = args; - const aptos = new Aptos(aptosConfig); + const { aptos } = getAptosClient(aptosConfig); const rawTransaction = await this.builder({ ...args, integrator: AccountAddress.ONE, @@ -733,7 +734,7 @@ export class SwapWithRewards extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const aptos = new Aptos(aptosConfig); + const { aptos } = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } diff --git a/src/typescript/sdk/src/utils/aptos-client.ts b/src/typescript/sdk/src/utils/aptos-client.ts index ac96e421e..e493d89af 100644 --- a/src/typescript/sdk/src/utils/aptos-client.ts +++ b/src/typescript/sdk/src/utils/aptos-client.ts @@ -1,22 +1,23 @@ -import { Aptos, AptosConfig, Network, NetworkToNetworkName } from "@aptos-labs/ts-sdk"; +import { Aptos, AptosConfig, type ClientConfig } from "@aptos-labs/ts-sdk"; +import { APTOS_API_KEY, APTOS_NETWORK } from "../const"; + +export const APTOS_CONFIG: Partial = { + API_KEY: APTOS_API_KEY, +}; export function getAptosClient(additionalConfig?: Partial): { aptos: Aptos; config: AptosConfig; } { - const network = getAptosNetwork(); + const network = APTOS_NETWORK; const config = new AptosConfig({ network, ...additionalConfig, + clientConfig: { + ...additionalConfig?.clientConfig, + ...APTOS_CONFIG, + }, }); const aptos = new Aptos(config); return { aptos, config }; } - -export function getAptosNetwork(): Network { - const networkRaw = process.env.NEXT_PUBLIC_APTOS_NETWORK; - if (!networkRaw) { - throw new Error("NEXT_PUBLIC_APTOS_NETWORK environment variable is not set."); - } - return networkRaw ? NetworkToNetworkName[networkRaw] : Network.LOCAL; -} diff --git a/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts b/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts index e5c0c12d7..a73fa918f 100644 --- a/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts +++ b/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts @@ -1,4 +1,6 @@ import { + APTOS_API_KEY, + APTOS_NETWORK, getEmojicoinMarketAddressAndTypeTags, INTEGRATOR_ADDRESS, INTEGRATOR_FEE_RATE_BPS, @@ -20,7 +22,7 @@ import { Network, } from "@aptos-labs/ts-sdk"; import { EXACT_TRANSITION_INPUT_AMOUNT } from "../../../../src/utils/test/helpers"; -import { getAptosNetwork } from "../../../../src/utils/aptos-client"; +import { getAptosClient } from "../../../../src/utils/aptos-client"; import { calculatePeriodBoundariesCrossed } from "../../../../src/utils/test"; jest.setTimeout(15000); @@ -103,9 +105,29 @@ describe("all submission types for the emojicoin client", () => { expect(emojicoinClient.aptos.config.network).toEqual(Network.TESTNET); }); + it("sets the API key in the aptos client configuration", () => { + const config = new AptosConfig({ + network: Network.TESTNET, + }); + const aptos = new Aptos(config); + const emojicoinClient = new EmojicoinClient({ aptos }); + expect(aptos.config.clientConfig?.API_KEY).toEqual(APTOS_API_KEY); + expect(emojicoinClient.aptos.config.clientConfig?.API_KEY).toEqual(APTOS_API_KEY); + }); + + it("sets the API key in the client returned by getAptos()", () => { + const config = new AptosConfig({ + network: Network.TESTNET, + }); + const { aptos } = getAptosClient(config); + const emojicoinClient = new EmojicoinClient({ aptos }); + expect(aptos.config.clientConfig?.API_KEY).toEqual(APTOS_API_KEY); + expect(emojicoinClient.aptos.config.clientConfig?.API_KEY).toEqual(APTOS_API_KEY); + }); + it("creates the aptos client with the correct default configuration settings", () => { expect(emojicoin.aptos.config.network).toEqual(process.env.NEXT_PUBLIC_APTOS_NETWORK); - expect(emojicoin.aptos.config.network).toEqual(getAptosNetwork()); + expect(emojicoin.aptos.config.network).toEqual(APTOS_NETWORK); }); it("registers a market", async () => { diff --git a/src/typescript/turbo.json b/src/typescript/turbo.json index 7392f70bd..72a8f32db 100644 --- a/src/typescript/turbo.json +++ b/src/typescript/turbo.json @@ -27,6 +27,12 @@ ".next/**" ] }, + "build:test": { + "outputs": [ + "dist/**", + ".next/**" + ] + }, "check": { "outputs": [] }, From 448f8c9bb9935a9d9e604a2bb27eebabfcda82fe Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Mon, 18 Nov 2024 20:19:10 -0800 Subject: [PATCH 57/94] [ECO-2442] Update default claim amount (#376) --- src/move/rewards/sources/emojicoin_dot_fun_claim_link.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/move/rewards/sources/emojicoin_dot_fun_claim_link.move b/src/move/rewards/sources/emojicoin_dot_fun_claim_link.move index 00a2395a9..c5e532ef8 100644 --- a/src/move/rewards/sources/emojicoin_dot_fun_claim_link.move +++ b/src/move/rewards/sources/emojicoin_dot_fun_claim_link.move @@ -17,7 +17,7 @@ module rewards::emojicoin_dot_fun_claim_link { use std::signer; const INTEGRATOR_FEE_RATE_BPS: u8 = 100; - const DEFAULT_CLAIM_AMOUNT: u64 = 100_000_000; + const DEFAULT_CLAIM_AMOUNT: u64 = 500_000_000; const VAULT: vector = b"Claim link vault"; /// Signer does not correspond to admin. From 219be644cbdf1ee9052956c0cb7420174ae0197c Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Tue, 19 Nov 2024 08:18:58 -0800 Subject: [PATCH 58/94] [ECO-2445] Lower refundable deposit amount, bump version (#379) --- src/move/emojicoin_dot_fun/Move.toml | 2 +- src/move/emojicoin_dot_fun/sources/emojicoin_dot_fun.move | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/move/emojicoin_dot_fun/Move.toml b/src/move/emojicoin_dot_fun/Move.toml index 3989b312c..0b50a8dcd 100644 --- a/src/move/emojicoin_dot_fun/Move.toml +++ b/src/move/emojicoin_dot_fun/Move.toml @@ -26,4 +26,4 @@ local = "../test_coin_factories/yellow_heart" authors = ["Econia Labs (developers@econialabs.com)"] name = "EmojicoinDotFun" upgrade_policy = "immutable" -version = "1.0.0" +version = "1.0.1" diff --git a/src/move/emojicoin_dot_fun/sources/emojicoin_dot_fun.move b/src/move/emojicoin_dot_fun/sources/emojicoin_dot_fun.move index 0bac7c39b..25f45462a 100644 --- a/src/move/emojicoin_dot_fun/sources/emojicoin_dot_fun.move +++ b/src/move/emojicoin_dot_fun/sources/emojicoin_dot_fun.move @@ -35,7 +35,7 @@ module emojicoin_dot_fun::emojicoin_dot_fun { const POOL_FEE_RATE_BPS: u8 = 25; /// Denominated in `AptosCoin` subunits. - const MARKET_REGISTRATION_DEPOSIT: u64 = 400_000_000; + const MARKET_REGISTRATION_DEPOSIT: u64 = 100_000_000; const MARKET_REGISTRATION_FEE: u64 = 100_000_000; /// Named object seed for the registry. From 87a60c66d8f68bcea66e547c6f6982a7f162f1dc Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:36:07 -0800 Subject: [PATCH 59/94] [ECO-2452] Change all references of 4 APT deposit to 1 APT in the docs (#380) --- doc/doc-site/docs/about/how-it-works.md | 6 +++--- doc/doc-site/docs/resources/faq.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/doc-site/docs/about/how-it-works.md b/doc/doc-site/docs/about/how-it-works.md index 29b5289b8..802990b1d 100644 --- a/doc/doc-site/docs/about/how-it-works.md +++ b/doc/doc-site/docs/about/how-it-works.md @@ -48,7 +48,7 @@ blockchain. ### First steps: launch your emojicoin -Launching an emojicoin costs just 1 APT plus a 4 APT refundable deposit! Note +Launching an emojicoin costs just 1 APT plus a 1 APT refundable deposit! Note the following: - Single emojis and emoji combinations are supported, as long as the total @@ -57,7 +57,7 @@ the following: markets are canonical). - Emojis that became part of the [Unicode emoji library] after the launch of emojicoin.fun are not supported. -- The 4 APT refundable deposit will be sent back to the market registrant once +- The 1 APT refundable deposit will be sent back to the market registrant once the market exits the bonding curve. - After the market is registered, a 5 minute grace period begins, during which only the market registrant may place the first swap. After the first swap or @@ -68,7 +68,7 @@ the following: As users buy or sell against the bonding curve, the market capitalization of the emojicoin changes. Once 1,000 APT of cumulative buy pressure has pushed the bonding curve up to a market capitalization of 4,500 APT, the emojicoin leaves -the bonding curve and the 4 APT deposit is automatically sent back to the market +the bonding curve and the 1 APT deposit is automatically sent back to the market registrant. ### Going to college: emojicoin liquidity pools diff --git a/doc/doc-site/docs/resources/faq.md b/doc/doc-site/docs/resources/faq.md index 044153926..ac3c44332 100644 --- a/doc/doc-site/docs/resources/faq.md +++ b/doc/doc-site/docs/resources/faq.md @@ -15,7 +15,7 @@ caught the emojicoin bug.* **Q: What is the cost to launch an Emojicoin?** -A: 1 APT, plus a 4 APT deposit that is automatically refunded after the market +A: 1 APT, plus a 1 APT deposit that is automatically refunded after the market exits the bonding curve From c2758480dfb3994e6c262d91e959aa9a9702e206 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Tue, 19 Nov 2024 20:44:25 +0100 Subject: [PATCH 60/94] [ECO-2450] Update refundable deposit amount (#381) --- src/typescript/sdk/src/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typescript/sdk/src/const.ts b/src/typescript/sdk/src/const.ts index 0c03b1f9e..ca4564672 100644 --- a/src/typescript/sdk/src/const.ts +++ b/src/typescript/sdk/src/const.ts @@ -94,7 +94,7 @@ export const BASE_VIRTUAL_CEILING = 4_900_000_000_000_000n; export const QUOTE_VIRTUAL_CEILING = 140_000_000_000n; export const POOL_FEE_RATE_BPS = 25; export const MARKET_REGISTRATION_FEE = ONE_APT_BIGINT; -export const MARKET_REGISTRATION_DEPOSIT = 4n * ONE_APT_BIGINT; +export const MARKET_REGISTRATION_DEPOSIT = 1n * ONE_APT_BIGINT; export const MARKET_REGISTRATION_GAS_ESTIMATION_NOT_FIRST = ONE_APT * 0.005; export const MARKET_REGISTRATION_GAS_ESTIMATION_FIRST = ONE_APT * 0.6; From 9348c4dcb418431d31260a50f0c8df20f00eed16 Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:13:07 -0800 Subject: [PATCH 61/94] [ECO-2448] Update audited git tag references (#382) --- README.md | 4 ++-- doc/doc-site/docs/resources/audit.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f7049cdd4..c7937f43a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The emojicoin dot fun Move package is audited: - [PDF Report] -- Corresponding `git` tag [`move-v1.0.0-audited`] +- Corresponding `git` tag [`move-v1.0.1-audited`] @@ -186,4 +186,4 @@ git submodule update --init --recursive [pre-commit shield]: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit [uploading environment variables with vercel's ui]: https://github.com/user-attachments/assets/d613725d-82ed-4a4e-a467-a89b2cf57d91 [vercel cli]: https://vercel.com/docs/cli -[`move-v1.0.0-audited`]: https://github.com/econia-labs/emojicoin-dot-fun/releases/tag/move-v1.0.0-audited +[`move-v1.0.1-audited`]: https://github.com/econia-labs/emojicoin-dot-fun/releases/tag/move-v1.0.1-audited diff --git a/doc/doc-site/docs/resources/audit.md b/doc/doc-site/docs/resources/audit.md index dd553ca40..a950afa85 100644 --- a/doc/doc-site/docs/resources/audit.md +++ b/doc/doc-site/docs/resources/audit.md @@ -7,7 +7,7 @@ hide_title: false The emojicoin dot fun Move package is audited: - [PDF Report] -- Corresponding `git` tag [`move-v1.0.0-audited`] +- Corresponding `git` tag [`move-v1.0.1-audited`] [pdf report]: https://econia-labs.notion.site/emojicoin-dot-fun-audit-8806ffea2b594c8e846ce3d32e5630b9 -[`move-v1.0.0-audited`]: https://github.com/econia-labs/emojicoin-dot-fun/releases/tag/move-v1.0.0-audited +[`move-v1.0.1-audited`]: https://github.com/econia-labs/emojicoin-dot-fun/releases/tag/move-v1.0.1-audited From 432b033e61d9c1b5fa9319ba9bd75e32d10cf50a Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:12:47 -0800 Subject: [PATCH 62/94] [ECO-2455] Update deploy file, indexer README (#383) --- src/cloud-formation/README.md | 8 ++++---- src/cloud-formation/deploy-indexer-production.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cloud-formation/README.md b/src/cloud-formation/README.md index 805f6f962..3471c27da 100644 --- a/src/cloud-formation/README.md +++ b/src/cloud-formation/README.md @@ -73,9 +73,7 @@ indexer deployments. A plaintext API key for the [transaction stream service endpoint] you are connecting to, for example an - [Aptos Labs transaction stream service API key]. Note that you'll need to - prepend `https://` as applicable if it is a public endpoint, for example - `https://grpc.devnet.aptoslabs.com:443` + [Aptos Labs transaction stream service API key]. @@ -103,7 +101,9 @@ indexer deployments. A [transaction stream service endpoint], for example the - [Aptos Labs gRPC endpoint]. + [Aptos Labs gRPC endpoint]. Note that you'll need to prepend `https://` as + applicable if it is a public endpoint, for example + `https://grpc.devnet.aptoslabs.com:443` diff --git a/src/cloud-formation/deploy-indexer-production.yaml b/src/cloud-formation/deploy-indexer-production.yaml index 4de5c7651..4c1b2c338 100644 --- a/src/cloud-formation/deploy-indexer-production.yaml +++ b/src/cloud-formation/deploy-indexer-production.yaml @@ -19,7 +19,7 @@ parameters: EnableWafRulesRestApi: 'false' EnableWafRulesWebSocket: 'false' Environment: 'production' - Network: 'devnet' + Network: 'mainnet' ProcessorImageVersion: '1.0.1' VpcStackName: 'emoji-vpc' tags: null From 45fe86938ee5a2767726ce738246f37ec615ad7b Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:41:00 -0800 Subject: [PATCH 63/94] Aptos API key cache hotfix (#385) --- src/typescript/frontend/src/app/market/[market]/page.tsx | 4 ++-- .../frontend/src/lib/queries/aptos-client/market-view.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/typescript/frontend/src/app/market/[market]/page.tsx b/src/typescript/frontend/src/app/market/[market]/page.tsx index 0a720e122..6d701b302 100644 --- a/src/typescript/frontend/src/app/market/[market]/page.tsx +++ b/src/typescript/frontend/src/app/market/[market]/page.tsx @@ -1,6 +1,6 @@ import ClientEmojicoinPage from "components/pages/emojicoin/ClientEmojicoinPage"; import EmojiNotFoundPage from "./not-found"; -import { fetchContractMarketView } from "lib/queries/aptos-client/market-view"; +import { cachedContractMarketView } from "lib/queries/aptos-client/market-view"; import { SYMBOL_EMOJI_DATA } from "@sdk/emoji_data"; import { pathToEmojiNames } from "utils/pathname-helpers"; import { fetchChatEvents, fetchMarketState, fetchSwapEvents } from "@/queries/market"; @@ -76,7 +76,7 @@ const EmojicoinPage = async (params: EmojicoinPageProps) => { const [chats, swaps, marketView] = await Promise.all([ fetchChatEvents({ marketID }), fetchSwapEvents({ marketID }), - fetchContractMarketView(marketAddress.toString()), + cachedContractMarketView(marketAddress.toString()), ]); return ( diff --git a/src/typescript/frontend/src/lib/queries/aptos-client/market-view.ts b/src/typescript/frontend/src/lib/queries/aptos-client/market-view.ts index 3646486d2..24b96e6f6 100644 --- a/src/typescript/frontend/src/lib/queries/aptos-client/market-view.ts +++ b/src/typescript/frontend/src/lib/queries/aptos-client/market-view.ts @@ -3,6 +3,7 @@ import { toMarketView } from "@sdk-types"; import { MarketView } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun"; import { getAptos } from "lib/utils/aptos-client"; +import { unstable_cache } from "next/cache"; export const fetchContractMarketView = async (marketAddress: `0x${string}`) => { const aptos = getAptos(); @@ -13,3 +14,11 @@ export const fetchContractMarketView = async (marketAddress: `0x${string}`) => { return toMarketView(res); }; + +export const cachedContractMarketView = unstable_cache( + fetchContractMarketView, + ["fetch-market-view"], + { + revalidate: 10, + } +); From 832f5961a5d86d6bf79872fedf25c6ada38ea8e8 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 19 Nov 2024 22:20:53 -0800 Subject: [PATCH 64/94] [ECO-2456] Add serialization fix for cached query (#387) --- .../frontend/src/app/market/[market]/page.tsx | 4 ++-- .../lib/queries/aptos-client/market-view.ts | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/typescript/frontend/src/app/market/[market]/page.tsx b/src/typescript/frontend/src/app/market/[market]/page.tsx index 6d701b302..f7857d00c 100644 --- a/src/typescript/frontend/src/app/market/[market]/page.tsx +++ b/src/typescript/frontend/src/app/market/[market]/page.tsx @@ -1,6 +1,6 @@ import ClientEmojicoinPage from "components/pages/emojicoin/ClientEmojicoinPage"; import EmojiNotFoundPage from "./not-found"; -import { cachedContractMarketView } from "lib/queries/aptos-client/market-view"; +import { wrappedCachedContractMarketView } from "lib/queries/aptos-client/market-view"; import { SYMBOL_EMOJI_DATA } from "@sdk/emoji_data"; import { pathToEmojiNames } from "utils/pathname-helpers"; import { fetchChatEvents, fetchMarketState, fetchSwapEvents } from "@/queries/market"; @@ -76,7 +76,7 @@ const EmojicoinPage = async (params: EmojicoinPageProps) => { const [chats, swaps, marketView] = await Promise.all([ fetchChatEvents({ marketID }), fetchSwapEvents({ marketID }), - cachedContractMarketView(marketAddress.toString()), + wrappedCachedContractMarketView(marketAddress.toString()), ]); return ( diff --git a/src/typescript/frontend/src/lib/queries/aptos-client/market-view.ts b/src/typescript/frontend/src/lib/queries/aptos-client/market-view.ts index 24b96e6f6..e5a82c780 100644 --- a/src/typescript/frontend/src/lib/queries/aptos-client/market-view.ts +++ b/src/typescript/frontend/src/lib/queries/aptos-client/market-view.ts @@ -4,6 +4,7 @@ import { toMarketView } from "@sdk-types"; import { MarketView } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun"; import { getAptos } from "lib/utils/aptos-client"; import { unstable_cache } from "next/cache"; +import { parseJSON, stringifyJSON } from "utils"; export const fetchContractMarketView = async (marketAddress: `0x${string}`) => { const aptos = getAptos(); @@ -12,13 +13,14 @@ export const fetchContractMarketView = async (marketAddress: `0x${string}`) => { marketAddress, }); - return toMarketView(res); + return stringifyJSON(toMarketView(res)); }; -export const cachedContractMarketView = unstable_cache( - fetchContractMarketView, - ["fetch-market-view"], - { - revalidate: 10, - } -); +const cachedContractMarketView = unstable_cache(fetchContractMarketView, ["fetch-market-view"], { + revalidate: 10, +}); + +export const wrappedCachedContractMarketView = async (marketAddress: `0x${string}`) => { + const cached = await cachedContractMarketView(marketAddress); + return parseJSON>(cached); +}; From ece61b776270ed4ac55737f0076f7e27c0895266 Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:33:40 -0800 Subject: [PATCH 65/94] [ECO-2459] Quadruple resources on DB, PostgREST, Broker (#389) --- src/cloud-formation/indexer.cfn.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cloud-formation/indexer.cfn.yaml b/src/cloud-formation/indexer.cfn.yaml index 1e1df2570..d4adc4c0b 100644 --- a/src/cloud-formation/indexer.cfn.yaml +++ b/src/cloud-formation/indexer.cfn.yaml @@ -110,7 +110,7 @@ Parameters: BrokerImageVersion: Type: 'String' DbMaxCapacity: - Default: 4 + Default: 16 Type: 'Number' DbMinCapacity: Default: 0.5 @@ -654,10 +654,10 @@ Resources: - 'Constants' - 'Networking' - 'BrokerPort' - Cpu: '256' + Cpu: '1024' ExecutionRoleArn: !GetAtt 'ContainerRole.Arn' Family: !Ref 'AWS::StackName' - Memory: '512' + Memory: '2048' NetworkMode: 'awsvpc' RequiresCompatibilities: - 'FARGATE' @@ -1305,10 +1305,10 @@ Resources: - 'Constants' - 'Networking' - 'PostgrestHealthCheckPort' - Cpu: '256' + Cpu: '1024' ExecutionRoleArn: !GetAtt 'ContainerRole.Arn' Family: !Ref 'AWS::StackName' - Memory: '512' + Memory: '2048' NetworkMode: 'awsvpc' RequiresCompatibilities: - 'FARGATE' From 5633a69d014b5fb9249e99833d6c710c737db13d Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Wed, 20 Nov 2024 12:40:16 +0100 Subject: [PATCH 66/94] [ECO-2463] Separate the actual txn simulation for gas usage calculation from the swap output calculation (#392) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- .../trade-emojicoin/SwapComponent.tsx | 34 +++++----- .../lib/hooks/queries/use-simulate-swap.ts | 65 ++++++++++++++----- 2 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx index 70588e749..5f41fae9f 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx @@ -8,7 +8,7 @@ import { SwapButton } from "./SwapButton"; import { type SwapComponentProps } from "components/pages/emojicoin/types"; import { toActualCoinDecimals, toDisplayCoinDecimals } from "lib/utils/decimals"; import { useScramble } from "use-scramble"; -import { useSimulateSwap } from "lib/hooks/queries/use-simulate-swap"; +import { DEFAULT_SWAP_GAS_COST, useSimulateSwap } from "lib/hooks/queries/use-simulate-swap"; import { useEventStore } from "context/event-store-context"; import { useTooltip } from "@hooks/index"; import { useSearchParams } from "next/navigation"; @@ -52,7 +52,6 @@ const inputAndOutputStyles = ` `; const OUTPUT_DISPLAY_DECIMALS = 4; -const SWAP_GAS_COST = 52500n; export default function SwapComponent({ emojicoin, @@ -78,10 +77,6 @@ export default function SwapComponent({ const [isSell, setIsSell] = useState(!(searchParams.get("sell") === null)); const [submit, setSubmit] = useState<(() => Promise) | null>(null); const { aptBalance, emojicoinBalance, account, setEmojicoinType } = useAptos(); - const availableAptBalance = useMemo( - () => (aptBalance - SWAP_GAS_COST > 0 ? aptBalance - SWAP_GAS_COST : 0n), - [aptBalance] - ); const [maxSlippage, setMaxSlippage] = useState(getMaxSlippageSettings().maxSlippage); @@ -106,18 +101,27 @@ export default function SwapComponent({ numSwaps, }); + const { swapResult, gasCost, gasCostWasUndefined } = swapData + ? { + swapResult: swapData.swapResult, + gasCost: swapData.gasCost, + gasCostWasUndefined: false, + } + : { + swapResult: undefined, + gasCost: DEFAULT_SWAP_GAS_COST, + gasCostWasUndefined: true, + }; + const outputAmountString = toDisplayCoinDecimals({ num: isLoading ? previous : outputAmount, decimals: OUTPUT_DISPLAY_DECIMALS, }); - let swapResult: bigint = 0n; - let gasCost: bigint | null = null; - - if (swapData) { - swapResult = swapData.swapResult; - gasCost = swapData.gasCost; - } + const availableAptBalance = useMemo( + () => (aptBalance - gasCost > 0 ? aptBalance - gasCost : 0n), + [gasCost, aptBalance] + ); const { ref, replay } = useScramble({ text: new Intl.NumberFormat().format(Number(outputAmountString)), @@ -295,9 +299,9 @@ export default function SwapComponent({
- {gasCost === null ? "~" : ""} + {gasCostWasUndefined ? "~" : ""} {toDisplayCoinDecimals({ - num: gasCost !== null ? gasCost.toString() : SWAP_GAS_COST.toString(), + num: gasCost, decimals: 4, })}{" "} APT diff --git a/src/typescript/frontend/src/lib/hooks/queries/use-simulate-swap.ts b/src/typescript/frontend/src/lib/hooks/queries/use-simulate-swap.ts index 1368a11e8..55fc97e45 100644 --- a/src/typescript/frontend/src/lib/hooks/queries/use-simulate-swap.ts +++ b/src/typescript/frontend/src/lib/hooks/queries/use-simulate-swap.ts @@ -14,19 +14,22 @@ import { useMemo } from "react"; import { toCoinTypes } from "@sdk/markets/utils"; import { type AccountInfo } from "@aptos-labs/wallet-adapter-core"; import { tryEd25519PublicKey } from "components/pages/launch-emojicoin/hooks/use-register-market"; -import { STRUCT_STRINGS } from "@sdk/utils"; -export const simulateSwap = async (args: { +type Args = { aptos: Aptos; account: AccountInfo | null; - swapper: AccountAddressString; + swapper: AccountAddressString | undefined; marketAddress: AccountAddressString; inputAmount: AnyNumber; isSell: boolean; minOutputAmount: AnyNumber; typeTags: [TypeTagInput, TypeTagInput]; -}) => { - if (args.account) { +}; + +export const DEFAULT_SWAP_GAS_COST = 52500n; + +const getGas = async (args: Args) => { + if (args.account && typeof args.swapper !== "undefined") { const publicKey = tryEd25519PublicKey(args.account); if (publicKey) { const res = await Swap.simulate({ @@ -41,15 +44,37 @@ export const simulateSwap = async (args: { minOutputAmount: args.minOutputAmount, typeTags: args.typeTags, }); - const swapEvent = res.events.find((e) => e.type === STRUCT_STRINGS.SwapEvent)!; return { - base_volume: swapEvent.data.base_volume, - quote_volume: swapEvent.data.quote_volume, gas_used: res.gas_used, gas_unit_price: res.gas_unit_price, }; } } + return { + gas_used: null, + gas_unit_price: null, + }; +}; + +const useGetGas = (args: Args) => { + const { data } = useQuery({ + queryKey: ["get-gas-price", args.aptos.config.network, args.swapper ?? ""], + queryFn: () => getGas(args), + staleTime: 20 * 1000, + }); + return data; +}; + +export const simulateSwap = async (args: { + aptos: Aptos; + account: AccountInfo | null; + swapper: AccountAddressString; + marketAddress: AccountAddressString; + inputAmount: AnyNumber; + isSell: boolean; + minOutputAmount: AnyNumber; + typeTags: [TypeTagInput, TypeTagInput]; +}) => { const res = await withResponseError( SimulateSwap.view({ ...args, @@ -60,8 +85,6 @@ export const simulateSwap = async (args: { return { base_volume: res.base_volume, quote_volume: res.quote_volume, - gas_used: null, - gas_unit_price: null, }; }; @@ -98,7 +121,7 @@ export const useSimulateSwap = (args: { marketAddress, inputAmount.toString(), isSell, - numSwaps, + Math.round(numSwaps / 10) * 10, emojicoin.toString(), emojicoinLP.toString(), swapper ?? "", @@ -109,8 +132,6 @@ export const useSimulateSwap = (args: { ? { quote_volume: "0", base_volume: "0", - gas_used: null, - gas_unit_price: null, } : simulateSwap({ aptos, @@ -121,16 +142,26 @@ export const useSimulateSwap = (args: { minOutputAmount, typeTags, }), - staleTime: Infinity, + staleTime: 10 * 1000, + }); + + const gas = useGetGas({ + aptos, + account, + ...args, + swapper, + inputAmount, + minOutputAmount, + typeTags, }); return typeof data === "undefined" ? data : { gasCost: - typeof data.gas_used === "string" && typeof data.gas_unit_price === "string" - ? BigInt(data.gas_used) * BigInt(data.gas_unit_price) - : null, + gas && typeof gas.gas_used === "string" && typeof gas.gas_unit_price === "string" + ? BigInt(gas.gas_used) * BigInt(gas.gas_unit_price) + : DEFAULT_SWAP_GAS_COST, swapResult: isSell ? BigInt(data.quote_volume) : BigInt(data.base_volume), }; }; From f05c7ef16f7a448160c57c3a3db25fdefaed0cbd Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Wed, 20 Nov 2024 06:55:47 -0800 Subject: [PATCH 67/94] =?UTF-8?q?[ECO-2465]=20Add=20=F0=9F=86=97=E2=9D=8C?= =?UTF-8?q?=20to=20the=20wallet=20adapter=20(#394)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/typescript/frontend/package.json | 1 + src/typescript/frontend/public/okx-logo.png | Bin 0 -> 3368 bytes .../src/components/svg/icons/OKXIcon.tsx | 23 ++++++++++++++++++ .../frontend/src/context/providers.tsx | 7 +++++- .../src/context/wallet-context/WalletItem.tsx | 6 ++++- src/typescript/pnpm-lock.yaml | 14 +++++++++++ 6 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/typescript/frontend/public/okx-logo.png create mode 100644 src/typescript/frontend/src/components/svg/icons/OKXIcon.tsx diff --git a/src/typescript/frontend/package.json b/src/typescript/frontend/package.json index 79efd0246..f615aefc9 100644 --- a/src/typescript/frontend/package.json +++ b/src/typescript/frontend/package.json @@ -24,6 +24,7 @@ "@martianwallet/aptos-wallet-adapter": "^0.0.5", "@next/bundle-analyzer": "^14.2.15", "@noble/hashes": "^1.5.0", + "@okwallet/aptos-wallet-adapter": "^0.0.7", "@pontem/wallet-adapter-plugin": "^0.2.1", "@popperjs/core": "^2.11.8", "@radix-ui/react-dropdown-menu": "^2.1.2", diff --git a/src/typescript/frontend/public/okx-logo.png b/src/typescript/frontend/public/okx-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ff67a24dba952e5ad980a014e3f260318524a608 GIT binary patch literal 3368 zcmeH~`%{u>6vtmJ@`6q6n3~atN2k=59`twot-mf9lYLK)>Pqj)7!w;}Jf zyD8pKGjs|=T`xcg3l&mnm={6=UMe;kFYx9nzB@{|HvI+twwIon^UE`5<~j2{=Q*Eq zat;#07P>BT1pr{--tR&W0f1xl2XkI9cOxaUbpYT7?+x8`_=1C3xBn6M$Cb+Q**}7= zK3ctY?OGBk8;#COUDLbU?VI8KOJYOO*B8@=yhE3^Ca&4U-4k(5y}ovB>(y$nKT(HT zt~bdZecx5G>uJgDa}~qP6!NQ^n2CHFQI&4#ak?a?&{U!xMAVZ`@R)n~^Z5h?lHJ6v zgl>@pO;7T}%72mg=5n{T&?1r!G`n!jcnB8@B*}$rgAQz1f#K9 zU`a$8eoILcg)XqFHeGfPvP2i4XZCq@a4o%%LV@+}wY@JJy(k>Ot3js2n9N&Z(8PL) zxNu`zF3BVi(`ZkvwwP(VTHFd08Vk!tUz2(7s|OcU@kAV2`wN&}@0s`Z8g8^gGZP87 zlCVaZSq#qP9Vdn@X*l^33yX0)YGTJ%OEUnIqurK3BT&j%Z-@KB5Eg zlZ!uaIVckaEMEWF=^re?Xvd-MF$IZyf|-Up=+BXnDp?Nkw~!0LwO@1^{5%g3n^i&u0}QThL^vDBSm;(#0C}OsF;IyHHz4@c@B z6G$YAc44iFZVzBE;mRML5jIA{1yE69x?Ffmz#BYlEL7?xqhR0?Eh*!0h`m?T5Zky# zpDkI0*bg0qf`&wXV3j2f!tpXXIyQEOiH!H{Y6#!9joGHMK+;-mBsD8VmBNIFjW#_9 zR+-~^arIF}KE8(VHo8qZG;a69!I&Dal(eoKt=c<%bN3xlt)TgJ{sJbC6J8je%7oq@a z!}8O7VUwCOS{WxB$nZn8_X|O*`8Z=urYIj%XnZ%>h-MdO|J+o$X(TSTd!)>Tb)qt|L9Wdnqb#2CCD_e@+T;IZe(Q8X};G7rEG zE|k1YAvbjPnzK!%k(o}nDWJez;q)}j2 z`dGG^@_fWfi@#{YD*kG>%#$cV78(jAHoL#IO8dloT4M95P5}xYX4D|cmA3bm)J{s| z3Z2}L-!x5vJ0WjHop-#o{J6w!@w>e8UIWroHdHkD2jNY53V>u(;vx_2%c(hnQF!5&fb-Zy6(I9KNtNL#i}XoS1Y>bqC`Af?$v#S&<1jQ zBYWtIqPxTX0YVnF&gS+&*I_rA-clFOQRmcuiaM7aSBL+92zu-zW74YGL;Z?wP9WK2 zWywf)XYxw*G2LUCPz@;x@yHYKUe4m~A}8VdBy6A+I$_D`wj7l#TGHYjZ<9jn6gNfL zifvAyptxl|W;!*8cP2R;ZX;n08&Xg!l+N$8^e}3fen=Zf%_|2RHL*Q%AJ|LMk0v^O zo4}|;QlF;R#Jo6RX51Fk4`MY_oI4^;QMmT%%f=hR*ZV8vqqiBAAI}1q1~=#F TXv5qt1OR*Y5JLI8iCKRGNQVr8 literal 0 HcmV?d00001 diff --git a/src/typescript/frontend/src/components/svg/icons/OKXIcon.tsx b/src/typescript/frontend/src/components/svg/icons/OKXIcon.tsx new file mode 100644 index 000000000..a14f208dd --- /dev/null +++ b/src/typescript/frontend/src/components/svg/icons/OKXIcon.tsx @@ -0,0 +1,23 @@ +import Image from "next/image"; + +const OKXIcon = ({ + width, + height, + className, +}: { + width: number; + height: number; + className: string; +}) => { + return ( + okx logo + ); +}; + +export default OKXIcon; diff --git a/src/typescript/frontend/src/context/providers.tsx b/src/typescript/frontend/src/context/providers.tsx index cccbec7e7..bab4f785b 100644 --- a/src/typescript/frontend/src/context/providers.tsx +++ b/src/typescript/frontend/src/context/providers.tsx @@ -2,6 +2,7 @@ // cspell:word martianwallet // cspell:word pontem +// cspell:word okwallet import React, { Suspense, useEffect, useMemo, useState } from "react"; import { ThemeProvider } from "styled-components"; import { GlobalStyle } from "styles"; @@ -26,6 +27,7 @@ import { WalletModalContextProvider } from "./wallet-context/WalletModalContext" import { PontemWallet } from "@pontem/wallet-adapter-plugin"; import { RiseWallet } from "@rise-wallet/wallet-adapter"; import { MartianWallet } from "@martianwallet/aptos-wallet-adapter"; +import { OKXWallet } from "@okwallet/aptos-wallet-adapter"; import { EmojiPickerProvider } from "./emoji-picker-context/EmojiPickerContextProvider"; import { isMobile, isTablet } from "react-device-detect"; import { APTOS_API_KEY } from "@sdk/const"; @@ -41,7 +43,10 @@ const ThemedApp: React.FC<{ children: React.ReactNode }> = ({ children }) => { const isMobileMenuOpen = isOpen && !isDesktop; - const wallets = useMemo(() => [new PontemWallet(), new RiseWallet(), new MartianWallet()], []); + const wallets = useMemo( + () => [new PontemWallet(), new RiseWallet(), new MartianWallet(), new OKXWallet()], + [] + ); return ( diff --git a/src/typescript/frontend/src/context/wallet-context/WalletItem.tsx b/src/typescript/frontend/src/context/wallet-context/WalletItem.tsx index 9798e8618..092e2ccfe 100644 --- a/src/typescript/frontend/src/context/wallet-context/WalletItem.tsx +++ b/src/typescript/frontend/src/context/wallet-context/WalletItem.tsx @@ -11,6 +11,7 @@ import PetraIcon from "@icons/PetraIcon"; import PontemIcon from "@icons/PontemIcon"; import RiseIcon from "@icons/RiseIcon"; import NightlyIcon from "@icons/NightlyIcon"; +import OKXIcon from "@icons/OKXIcon"; import { Arrow } from "components/svg"; import { useScramble } from "use-scramble"; import { Emoji } from "utils/emoji"; @@ -23,6 +24,7 @@ const IconProps = { }; export const WALLET_ICON: { [key: string]: ReactElement } = { + "okx wallet": , petra: , pontem: , martian: , @@ -40,7 +42,9 @@ export const walletSort = ( }; export const isSupportedWallet = (s: string) => { - return Object.keys(WALLET_ICON).includes(s.toLowerCase()); + return Object.keys(WALLET_ICON) + .map((w) => w.toLowerCase()) + .includes(s.toLowerCase()); }; const WalletNameClassName = "ml-4 font-pixelar text-[20px] text-black uppercase flex"; diff --git a/src/typescript/pnpm-lock.yaml b/src/typescript/pnpm-lock.yaml index 4f5899249..95173d1c0 100644 --- a/src/typescript/pnpm-lock.yaml +++ b/src/typescript/pnpm-lock.yaml @@ -66,6 +66,9 @@ importers: '@noble/hashes': specifier: ^1.5.0 version: 1.5.0 + '@okwallet/aptos-wallet-adapter': + specifier: ^0.0.7 + version: 0.0.7(@aptos-labs/wallet-adapter-core@4.17.0(@aptos-labs/ts-sdk@1.27.1)(@mizuwallet-sdk/core@1.3.2(@aptos-labs/ts-sdk@1.27.1)(graphql-request@7.1.0(graphql@16.9.0)))(@mizuwallet-sdk/protocol@0.0.1)(@telegram-apps/bridge@1.2.1)(@wallet-standard/core@1.0.3)(aptos@1.21.0))(aptos@1.21.0) '@pontem/wallet-adapter-plugin': specifier: ^0.2.1 version: 0.2.1 @@ -1230,6 +1233,12 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@okwallet/aptos-wallet-adapter@0.0.7': + resolution: {integrity: sha512-n6Uhh8uzj6pRRXppKYUPRDxzb3x/wH9xy3uXqYMbjJbiPb5LrWhm8LYYkRpw3ZFFNH3B/taMILd2qygzWVDH5w==} + peerDependencies: + '@aptos-labs/wallet-adapter-core': 3.5.0 + aptos: ^1.21.0 + '@pedrouid/environment@1.0.1': resolution: {integrity: sha512-HaW78NszGzRZd9SeoI3JD11JqY+lubnaOx7Pewj5pfjqWXOEATpeKIFb9Z4t2WBUK2iryiXX3lzWwmYWgUL0Ug==} @@ -5746,6 +5755,11 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@okwallet/aptos-wallet-adapter@0.0.7(@aptos-labs/wallet-adapter-core@4.17.0(@aptos-labs/ts-sdk@1.27.1)(@mizuwallet-sdk/core@1.3.2(@aptos-labs/ts-sdk@1.27.1)(graphql-request@7.1.0(graphql@16.9.0)))(@mizuwallet-sdk/protocol@0.0.1)(@telegram-apps/bridge@1.2.1)(@wallet-standard/core@1.0.3)(aptos@1.21.0))(aptos@1.21.0)': + dependencies: + '@aptos-labs/wallet-adapter-core': 4.17.0(@aptos-labs/ts-sdk@1.27.1)(@mizuwallet-sdk/core@1.3.2(@aptos-labs/ts-sdk@1.27.1)(graphql-request@7.1.0(graphql@16.9.0)))(@mizuwallet-sdk/protocol@0.0.1)(@telegram-apps/bridge@1.2.1)(@wallet-standard/core@1.0.3)(aptos@1.21.0) + aptos: 1.21.0 + '@pedrouid/environment@1.0.1': {} '@pkgjs/parseargs@0.11.0': From cf2afe61dba0358496ff91fa131f5a6b7c116fad Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:14:47 -0800 Subject: [PATCH 68/94] [ECO-2467] Add back in autoConnect for the wallet adapter (#396) --- src/typescript/frontend/src/context/providers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typescript/frontend/src/context/providers.tsx b/src/typescript/frontend/src/context/providers.tsx index bab4f785b..7fb8f5443 100644 --- a/src/typescript/frontend/src/context/providers.tsx +++ b/src/typescript/frontend/src/context/providers.tsx @@ -55,7 +55,7 @@ const ThemedApp: React.FC<{ children: React.ReactNode }> = ({ children }) => { Date: Wed, 20 Nov 2024 10:17:42 -0800 Subject: [PATCH 69/94] [ECO-2469] Remove galxe campaign fetch in allowlist checks (#398) --- .../src/components/pages/verify/session.ts | 4 +- .../pages/verify_status/VerifyStatusPage.tsx | 7 +-- .../verify_status/get-verification-status.ts | 14 ++---- src/typescript/frontend/src/lib/server-env.ts | 7 +-- .../frontend/src/lib/utils/allowlist.ts | 44 ++----------------- 5 files changed, 11 insertions(+), 65 deletions(-) diff --git a/src/typescript/frontend/src/components/pages/verify/session.ts b/src/typescript/frontend/src/components/pages/verify/session.ts index fc54cd0d3..44054a87f 100644 --- a/src/typescript/frontend/src/components/pages/verify/session.ts +++ b/src/typescript/frontend/src/components/pages/verify/session.ts @@ -22,14 +22,14 @@ export const createSession = async (address: AccountAddressString) => { cookies().set(COOKIE_FOR_HASHED_ADDRESS, hashed, { httpOnly: true, - secure: process.env.NODE_ENV === "production", + secure: process.env.NODE_ENV === "production" || process.env.VERCEL === "1", maxAge: COOKIE_LENGTH, path: "/", }); cookies().set(COOKIE_FOR_ACCOUNT_ADDRESS, address, { httpOnly: true, - secure: process.env.NODE_ENV === "production", + secure: process.env.NODE_ENV === "production" || process.env.VERCEL === "1", maxAge: COOKIE_LENGTH, path: "/", }); diff --git a/src/typescript/frontend/src/components/pages/verify_status/VerifyStatusPage.tsx b/src/typescript/frontend/src/components/pages/verify_status/VerifyStatusPage.tsx index 1400e955e..35faa8294 100644 --- a/src/typescript/frontend/src/components/pages/verify_status/VerifyStatusPage.tsx +++ b/src/typescript/frontend/src/components/pages/verify_status/VerifyStatusPage.tsx @@ -6,7 +6,7 @@ import { useAptos } from "context/wallet-context/AptosContextProvider"; import { useEffect, useState } from "react"; import { motion } from "framer-motion"; import { standardizeAddress, truncateAddress } from "@sdk/utils"; -import { getVerificationStatus } from "./get-verification-status"; +import { getIsOnCustomAllowlist } from "./get-verification-status"; import { EXTERNAL_LINK_PROPS } from "components/link"; import { emoji } from "utils"; import { Emoji } from "utils/emoji"; @@ -32,10 +32,7 @@ export const ClientVerifyPage = () => { setCustomAllowlisted(false); } else { const address = standardizeAddress(account.address); - getVerificationStatus(address).then(({ galxe, customAllowlisted }) => { - setGalxe(galxe); - setCustomAllowlisted(customAllowlisted); - }); + getIsOnCustomAllowlist(address).then((res) => setCustomAllowlisted(res)); } }, [account, connected]); diff --git a/src/typescript/frontend/src/components/pages/verify_status/get-verification-status.ts b/src/typescript/frontend/src/components/pages/verify_status/get-verification-status.ts index 085f83253..132929945 100644 --- a/src/typescript/frontend/src/components/pages/verify_status/get-verification-status.ts +++ b/src/typescript/frontend/src/components/pages/verify_status/get-verification-status.ts @@ -1,14 +1,6 @@ "use server"; +import { isOnCustomAllowlist } from "lib/utils/allowlist"; -import { isInGalxeCampaign, isOnCustomAllowlist } from "lib/utils/allowlist"; - -export async function getVerificationStatus(address: `0x${string}`) { - const [galxe, customAllowlisted] = await Promise.all([ - isInGalxeCampaign(address), - isOnCustomAllowlist(address), - ]); - return { - galxe, - customAllowlisted, - }; +export async function getIsOnCustomAllowlist(address: `0x${string}`) { + return await isOnCustomAllowlist(address); } diff --git a/src/typescript/frontend/src/lib/server-env.ts b/src/typescript/frontend/src/lib/server-env.ts index 9d2966f02..43d78dff6 100644 --- a/src/typescript/frontend/src/lib/server-env.ts +++ b/src/typescript/frontend/src/lib/server-env.ts @@ -12,11 +12,7 @@ if (!process.env.HASH_SEED || process.env.HASH_SEED.length < 8) { throw new Error("Environment variable HASH_SEED must be set and at least 8 characters."); } -if ( - IS_ALLOWLIST_ENABLED && - typeof process.env.ALLOWLISTER3K_URL === "undefined" && - typeof process.env.GALXE_CAMPAIGN_ID === "undefined" -) { +if (IS_ALLOWLIST_ENABLED && typeof process.env.ALLOWLISTER3K_URL === "undefined") { throw new Error("Allowlist is enabled but no allowlist provider is set."); } @@ -38,7 +34,6 @@ if (GEOBLOCKING_ENABLED) { } export const ALLOWLISTER3K_URL: string | undefined = process.env.ALLOWLISTER3K_URL; -export const GALXE_CAMPAIGN_ID: string | undefined = process.env.GALXE_CAMPAIGN_ID; export const REVALIDATION_TIME: number = Number(process.env.REVALIDATION_TIME); export const VPNAPI_IO_API_KEY: string = process.env.VPNAPI_IO_API_KEY!; export const PRE_LAUNCH_TEASER: boolean = process.env.PRE_LAUNCH_TEASER === "true"; diff --git a/src/typescript/frontend/src/lib/utils/allowlist.ts b/src/typescript/frontend/src/lib/utils/allowlist.ts index 1308b5844..641062f43 100644 --- a/src/typescript/frontend/src/lib/utils/allowlist.ts +++ b/src/typescript/frontend/src/lib/utils/allowlist.ts @@ -1,11 +1,9 @@ import "server-only"; import { IS_ALLOWLIST_ENABLED } from "lib/env"; -import { ALLOWLISTER3K_URL, GALXE_CAMPAIGN_ID } from "lib/server-env"; +import { ALLOWLISTER3K_URL } from "lib/server-env"; -export const GALXE_URL = "https://graphigo.prd.galaxy.eco/query"; - -// Checks if the given address is allow listed either in Galxe or in Allowlister3000. +// Checks if the given address is allow listed in Allowlister3000. // // If IS_ALLOWLIST_ENABLED is not truthy, the function returns true. // @@ -19,45 +17,9 @@ export async function isAllowListed(addressIn: string): Promise { ? (addressIn as `0x${string}`) : (`0x${addressIn}` as const); - const [inCampaign, onAllowlist] = await Promise.all([ - isInGalxeCampaign(address), - isOnCustomAllowlist(address), - ]); - return inCampaign || onAllowlist; + return await isOnCustomAllowlist(address); } -export const isInGalxeCampaign = async (address: `0x${string}`): Promise => { - if (GALXE_CAMPAIGN_ID !== undefined) { - const condition = await fetch(GALXE_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ - query: `{ - campaign(id: "${GALXE_CAMPAIGN_ID}") { - whitelistInfo(address: "${address}") { - maxCount - usedCount - claimedLoyaltyPoints - currentPeriodMaxLoyaltyPoints - currentPeriodClaimedLoyaltyPoints - } - } - }`, - }), - }) - .then((r) => r.json()) - .then((data) => data.data.campaign && data.data.campaign.whitelistInfo.usedCount === 1); - if (condition) { - return true; - } - } - - return false; -}; - export const isOnCustomAllowlist = async (address: `0x${string}`): Promise => { if (ALLOWLISTER3K_URL !== undefined) { const condition = await fetch(`${ALLOWLISTER3K_URL}/${address}`) From b5bb19e01c9bf6e244c2dc6acd199b882ff22b82 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:37:03 -0800 Subject: [PATCH 70/94] [ECO-2470] Add okx icon/logo to middleware.ts so it shows up in the wallet adapter on `/verify` (#399) --- .gitignore | 2 ++ src/typescript/frontend/src/middleware.ts | 13 ++++--------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 505d79709..2abf10303 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ **/.env **/.turbo +**/.vercel +**/.env*.local diff --git a/src/typescript/frontend/src/middleware.ts b/src/typescript/frontend/src/middleware.ts index b324fcfcb..730f066a7 100644 --- a/src/typescript/frontend/src/middleware.ts +++ b/src/typescript/frontend/src/middleware.ts @@ -20,14 +20,7 @@ export default async function middleware(request: NextRequest) { if (MAINTENANCE_MODE && pathname !== "/maintenance") { return NextResponse.redirect(new URL(ROUTES.maintenance, request.url)); } - if ( - pathname === "/social-preview.png" || - pathname === "/webclip.png" || - pathname === "/icon.png" || - pathname === "/test" || - pathname === "/geolocation" || - pathname === "/verify_status" - ) { + if (pathname === "/test" || pathname === "/verify_status") { return NextResponse.next(); } @@ -63,6 +56,8 @@ export default async function middleware(request: NextRequest) { return NextResponse.next(); } +// Note this must be a static string- we can't dynamically construct it. export const config = { - matcher: "/((?!verify|api|_next/static|_next/image|favicon.ico|logo192.png|manifest.json).*)", + /* eslint-disable-next-line */ + matcher: `/((?!verify|api|_next/static|_next/image|favicon.ico|logo192.png|icon.png|webclip.png|social-preview.png|okx-logo.png|manifest.json).*)`, }; From 10217252d78afc8e491bf21fb03a736a81050938 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Thu, 21 Nov 2024 06:50:52 +0100 Subject: [PATCH 71/94] [ECO-2460] Implement server side api key (#390) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- cfg/cspell-dictionary.txt | 1 + src/typescript/example.env | 13 +++++ src/typescript/frontend/next.config.mjs | 29 +++++++---- src/typescript/frontend/src/app/test/route.ts | 4 +- .../frontend/src/app/verify_api_keys/page.tsx | 51 +++++++++++++++---- .../src/components/charts/PrivateChart.tsx | 4 +- .../frontend/src/context/providers.tsx | 4 +- .../wallet-context/AptosContextProvider.tsx | 8 ++- .../src/lib/hooks/queries/use-num-markets.ts | 4 +- .../lib/queries/aptos-client/market-view.ts | 4 +- .../frontend/src/lib/utils/aptos-client.ts | 41 --------------- .../sdk/src/client/emojicoin-client.ts | 2 +- src/typescript/sdk/src/const.ts | 27 +++++----- .../src/emojicoin_dot_fun/aptos-framework.ts | 6 +-- .../emojicoin_dot_fun/emojicoin-dot-fun.ts | 16 +++--- .../sdk/src/indexer-v2/queries/app/home.ts | 2 +- .../sdk/src/indexer-v2/queries/utils.ts | 8 +-- src/typescript/sdk/src/utils/aptos-client.ts | 45 ++++++++++++---- .../test/get-publish-txn-from-indexer.ts | 2 +- src/typescript/sdk/src/utils/test/helpers.ts | 2 +- src/typescript/sdk/src/utils/test/publish.ts | 4 +- .../sdk/tests/e2e/broker/websockets.test.ts | 2 +- src/typescript/sdk/tests/e2e/fund.test.ts | 2 +- .../sdk/tests/e2e/queries/address.test.ts | 2 +- .../tests/e2e/queries/client/submit.test.ts | 22 -------- .../tests/e2e/queries/market-state.test.ts | 2 +- .../sdk/tests/e2e/queries/num-markets.test.ts | 2 +- .../sdk/tests/e2e/queries/simple.test.ts | 2 +- .../e2e/queries/sorted/sort-queries.test.ts | 2 +- .../sdk/tests/e2e/queries/volume.test.ts | 2 +- 30 files changed, 167 insertions(+), 148 deletions(-) delete mode 100644 src/typescript/frontend/src/lib/utils/aptos-client.ts diff --git a/cfg/cspell-dictionary.txt b/cfg/cspell-dictionary.txt index 1f7bbae7f..1f81c6aa2 100644 --- a/cfg/cspell-dictionary.txt +++ b/cfg/cspell-dictionary.txt @@ -27,6 +27,7 @@ dasharray defi devnet diya +dockerenv dockerhub econia econialabs diff --git a/src/typescript/example.env b/src/typescript/example.env index 1ad12d558..2193a8941 100644 --- a/src/typescript/example.env +++ b/src/typescript/example.env @@ -68,3 +68,16 @@ PUBLISHER_PRIVATE_KEY="" # The URL to connect to the PostgreSQL database. # Useful in testing only. DB_URL="" + +# Set the API keys for the various frontend networks. It is okay if these are exposed publicly. +NEXT_PUBLIC_LOCAL_APTOS_API_KEY="" +NEXT_PUBLIC_CUSTOM_APTOS_API_KEY="" +NEXT_PUBLIC_DEVNET_APTOS_API_KEY="" +NEXT_PUBLIC_TESTNET_APTOS_API_KEY="" +NEXT_PUBLIC_MAINNET_APTOS_API_KEY="" +# These should NOT be exposed publicly. +SERVER_LOCAL_APTOS_API_KEY="" +SERVER_CUSTOM_APTOS_API_KEY="" +SERVER_DEVNET_APTOS_API_KEY="" +SERVER_TESTNET_APTOS_API_KEY="" +SERVER_MAINNET_APTOS_API_KEY="" diff --git a/src/typescript/frontend/next.config.mjs b/src/typescript/frontend/next.config.mjs index e6435a94e..32772c088 100644 --- a/src/typescript/frontend/next.config.mjs +++ b/src/typescript/frontend/next.config.mjs @@ -26,11 +26,6 @@ const debugConfigOptions = { static: 30, // Default is normally 180s. }, }, - logging: { - fetches: { - fullUrl: true, - }, - }, }; /** @type {import('next').NextConfig} */ @@ -44,12 +39,26 @@ const nextConfig = { styledComponents: DEBUG ? styledComponentsConfig : true, }, ...(DEBUG ? debugConfigOptions : {}), + // Log full fetch URLs if we're in a specific environment. + logging: + process.env.NODE_ENV === "development" || + process.env.NODE_ENV === "test" || + process.env.VERCEL_ENV === "preview" || + process.env.VERCEL_ENV === "development" + ? { + fetches: { + fullUrl: true, + }, + } + : undefined, transpilePackages: ["@sdk"], - redirects: async () => ([{ - source: '/', - destination: '/home', - permanent: true, - }]), + redirects: async () => [ + { + source: "/", + destination: "/home", + permanent: true, + }, + ], }; export default withBundleAnalyzer(nextConfig); diff --git a/src/typescript/frontend/src/app/test/route.ts b/src/typescript/frontend/src/app/test/route.ts index b2681c6c3..f47b98584 100644 --- a/src/typescript/frontend/src/app/test/route.ts +++ b/src/typescript/frontend/src/app/test/route.ts @@ -1,4 +1,4 @@ -import { getAptos } from "lib/utils/aptos-client"; +import { getAptosClient } from "@sdk/utils/aptos-client"; import { NextResponse } from "next/server"; export const revalidate = 2; @@ -8,7 +8,7 @@ export async function GET() { if (process.env.NODE_ENV !== "test") { return new NextResponse("-1"); } - const aptos = getAptos(); + const aptos = getAptosClient(); try { const version = await aptos.getLedgerInfo().then((res) => res.ledger_version); return new NextResponse(version.toString()); diff --git a/src/typescript/frontend/src/app/verify_api_keys/page.tsx b/src/typescript/frontend/src/app/verify_api_keys/page.tsx index 040d57afb..b4cf5099c 100644 --- a/src/typescript/frontend/src/app/verify_api_keys/page.tsx +++ b/src/typescript/frontend/src/app/verify_api_keys/page.tsx @@ -1,5 +1,6 @@ +import { fetchMarketsWithCount } from "@/queries/home"; import { AccountAddress } from "@aptos-labs/ts-sdk"; -import { APTOS_API_KEY } from "@sdk/const"; +import { VERCEL } from "@sdk/const"; import { getAptosClient } from "@sdk/utils/aptos-client"; export const dynamic = "force-static"; @@ -7,19 +8,51 @@ export const revalidate = 600; export const runtime = "nodejs"; const VerifyApiKeys = async () => { - const { aptos } = getAptosClient(); + if (VERCEL === false) return <>; + /* eslint-disable-next-line no-console */ + console.warn("The API keys are being verified."); + + const network = process.env.NEXT_PUBLIC_APTOS_NETWORK?.toUpperCase(); + const serverKey = process.env[`SERVER_${network}_APTOS_API_KEY`]; + const clientKey = process.env[`NEXT_PUBLIC_${network}_APTOS_API_KEY`]; + if (!clientKey) { + throw new Error("Client Aptos API key not set."); + } + if (!serverKey) { + throw new Error("Server Aptos API key not set."); + } + if (serverKey === clientKey) { + throw new Error("Server Aptos API and client Aptos key are the same."); + } + + const clientAptos = getAptosClient({ clientConfig: { API_KEY: clientKey } }); + const serverAptos = getAptosClient({ clientConfig: { API_KEY: serverKey } }); + const accountAddress = AccountAddress.ONE; - let balance = 0; + + // Check that the client-side Aptos API key works. try { - balance = await aptos.account.getAccountAPTAmount({ accountAddress }); + await clientAptos.account.getAccountAPTAmount({ accountAddress }); } catch (e) { - const msg = `\n\tLikely an invalid API key. APTOS_API_KEY: ${APTOS_API_KEY}`; - throw new Error(`Couldn't fetch ${accountAddress}'s balance. ${msg}`); + const msg = "\n\tLikely an invalid client API key."; + throw new Error(`Couldn't fetch ${accountAddress}'s balance on the client. ${msg}`); + } + + // Check that the server-side Aptos API key works. + try { + await serverAptos.account.getAccountAPTAmount({ accountAddress }); + } catch (e) { + const msg = "\n\tLikely an invalid server API key."; + throw new Error(`Couldn't fetch ${accountAddress}'s balance on the server. ${msg}`); + } + + const res = await fetchMarketsWithCount({}); + if (res.error) { + const msg = "\n\tLikely an invalid indexer API key."; + throw new Error(`Couldn't fetch the price feed on the server. ${msg}`); } - return ( -
{`Balance: ${balance}`}
- ); + return
LGTM
; }; export default VerifyApiKeys; diff --git a/src/typescript/frontend/src/components/charts/PrivateChart.tsx b/src/typescript/frontend/src/components/charts/PrivateChart.tsx index ff10a9499..816f294cb 100644 --- a/src/typescript/frontend/src/components/charts/PrivateChart.tsx +++ b/src/typescript/frontend/src/components/charts/PrivateChart.tsx @@ -28,7 +28,6 @@ import path from "path"; import { emojisToName } from "lib/utils/emojis-to-name-or-symbol"; import { useEventStore } from "context/event-store-context"; import { getPeriodStartTimeFromTime } from "@sdk/utils"; -import { getAptos } from "lib/utils/aptos-client"; import { getSymbolEmojisInString, symbolToEmojis, toMarketEmojiData } from "@sdk/emoji_data"; import { type PeriodicStateEventModel, type MarketMetadataModel } from "@sdk/indexer-v2/types"; import { getMarketResource } from "@sdk/markets"; @@ -41,6 +40,7 @@ import { } from "@/store/event/candlestick-bars"; import { emoji, parseJSON } from "utils"; import { Emoji } from "utils/emoji"; +import { getAptosClient } from "@sdk/utils/aptos-client"; const configurationData: DatafeedConfiguration = { supported_resolutions: TV_CHARTING_LIBRARY_RESOLUTIONS, @@ -183,7 +183,7 @@ export const Chart = (props: ChartContainerProps) => { // Also, we specifically call this client-side because the server will get rate-limited if we call the // fullnode from the server for each client. const marketResource = await getMarketResource({ - aptos: getAptos(), + aptos: getAptosClient(), marketAddress: props.marketAddress, }); diff --git a/src/typescript/frontend/src/context/providers.tsx b/src/typescript/frontend/src/context/providers.tsx index 7fb8f5443..c9f866934 100644 --- a/src/typescript/frontend/src/context/providers.tsx +++ b/src/typescript/frontend/src/context/providers.tsx @@ -30,7 +30,7 @@ import { MartianWallet } from "@martianwallet/aptos-wallet-adapter"; import { OKXWallet } from "@okwallet/aptos-wallet-adapter"; import { EmojiPickerProvider } from "./emoji-picker-context/EmojiPickerContextProvider"; import { isMobile, isTablet } from "react-device-detect"; -import { APTOS_API_KEY } from "@sdk/const"; +import { getAptosApiKey } from "@sdk/const"; enableMapSet(); @@ -57,7 +57,7 @@ const ThemedApp: React.FC<{ children: React.ReactNode }> = ({ children }) => { plugins={wallets} autoConnect={true} dappConfig={{ - aptosApiKey: APTOS_API_KEY, + aptosApiKey: getAptosApiKey(), network: APTOS_NETWORK, }} > diff --git a/src/typescript/frontend/src/context/wallet-context/AptosContextProvider.tsx b/src/typescript/frontend/src/context/wallet-context/AptosContextProvider.tsx index a5c1cea63..ea1263ce8 100644 --- a/src/typescript/frontend/src/context/wallet-context/AptosContextProvider.tsx +++ b/src/typescript/frontend/src/context/wallet-context/AptosContextProvider.tsx @@ -23,7 +23,6 @@ import { import { toast } from "react-toastify"; import { type EntryFunctionTransactionBuilder } from "@sdk/emojicoin_dot_fun/payload-builders"; -import { getAptos } from "lib/utils/aptos-client"; import { checkNetworkAndToast, parseAPIErrorAndToast, @@ -41,6 +40,7 @@ import { import { getEventsAsProcessorModelsFromResponse } from "@sdk/mini-processor"; import { emoji } from "utils"; import useIsUserGeoblocked from "@hooks/use-is-user-geoblocked"; +import { getAptosClient } from "@sdk/utils/aptos-client"; type WalletContextState = ReturnType; export type SubmissionResponse = Promise<{ @@ -103,10 +103,8 @@ export function AptosContextProvider({ children }: PropsWithChildren) { }, [emojicoinType]); const aptos = useMemo(() => { - if (checkNetworkAndToast(network)) { - return getAptos(); - } - return getAptos(); + checkNetworkAndToast(network); + return getAptosClient(); }, [network]); const { diff --git a/src/typescript/frontend/src/lib/hooks/queries/use-num-markets.ts b/src/typescript/frontend/src/lib/hooks/queries/use-num-markets.ts index 00f01301a..be5f71cd4 100644 --- a/src/typescript/frontend/src/lib/hooks/queries/use-num-markets.ts +++ b/src/typescript/frontend/src/lib/hooks/queries/use-num-markets.ts @@ -1,10 +1,10 @@ import { RegistryView } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun"; import { useQuery } from "@tanstack/react-query"; -import { getAptos } from "lib/utils/aptos-client"; import { useEventStore } from "context/event-store-context"; +import { getAptosClient } from "@sdk/utils/aptos-client"; async function getNumMarkets(): Promise { - const aptos = getAptos(); + const aptos = getAptosClient(); return RegistryView.view({ aptos }).then((res) => Number(res.n_markets)); } diff --git a/src/typescript/frontend/src/lib/queries/aptos-client/market-view.ts b/src/typescript/frontend/src/lib/queries/aptos-client/market-view.ts index e5a82c780..1e6c4c1f2 100644 --- a/src/typescript/frontend/src/lib/queries/aptos-client/market-view.ts +++ b/src/typescript/frontend/src/lib/queries/aptos-client/market-view.ts @@ -2,12 +2,12 @@ import { toMarketView } from "@sdk-types"; import { MarketView } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun"; -import { getAptos } from "lib/utils/aptos-client"; +import { getAptosClient } from "@sdk/utils/aptos-client"; import { unstable_cache } from "next/cache"; import { parseJSON, stringifyJSON } from "utils"; export const fetchContractMarketView = async (marketAddress: `0x${string}`) => { - const aptos = getAptos(); + const aptos = getAptosClient(); const res = await MarketView.view({ aptos, marketAddress, diff --git a/src/typescript/frontend/src/lib/utils/aptos-client.ts b/src/typescript/frontend/src/lib/utils/aptos-client.ts deleted file mode 100644 index 2adbc8b45..000000000 --- a/src/typescript/frontend/src/lib/utils/aptos-client.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-disable import/no-unused-modules */ // Used in the frontend repo. -/* eslint-disable @typescript-eslint/no-var-requires */ -import { NetworkToFaucetAPI, NetworkToIndexerAPI, NetworkToNodeAPI } from "@aptos-labs/ts-sdk"; -import { Aptos, AptosConfig, NetworkToNetworkName } from "@aptos-labs/ts-sdk"; -import { APTOS_CONFIG } from "@sdk/utils/aptos-client"; -import { APTOS_NETWORK } from "lib/env"; - -const toDockerUrl = (url: string) => url.replace("127.0.0.1", "host.docker.internal"); - -// Get an Aptos config based off of the network environment variables and the default APTOS_CONFIG -// client configuration/settings. -export const getAptosConfig = (): AptosConfig => { - const networkString = APTOS_NETWORK; - const clientConfig = APTOS_CONFIG; - if (networkString === "local" && typeof window === "undefined") { - const fs = require("node:fs"); - if (fs.existsSync("/.dockerenv")) { - return new AptosConfig({ - network: NetworkToNetworkName["local"], - fullnode: toDockerUrl(NetworkToNodeAPI["local"]), - indexer: toDockerUrl(NetworkToIndexerAPI["local"]), - faucet: toDockerUrl(NetworkToFaucetAPI["local"]), - clientConfig, - }); - } - } - const network = NetworkToNetworkName[networkString ?? APTOS_NETWORK]; - const fullnode = NetworkToNodeAPI[network]; - const indexer = NetworkToIndexerAPI[network]; - const faucet = NetworkToFaucetAPI[network]; - if (!network) { - throw new Error(`Invalid network: ${networkString}`); - } - return new AptosConfig({ network, fullnode, indexer, faucet, clientConfig }); -}; - -// Get an Aptos client based off of the network environment variables. -export const getAptos = (): Aptos => { - const cfg = getAptosConfig(); - return new Aptos(cfg); -}; diff --git a/src/typescript/sdk/src/client/emojicoin-client.ts b/src/typescript/sdk/src/client/emojicoin-client.ts index 7fe497214..a38ab620a 100644 --- a/src/typescript/sdk/src/client/emojicoin-client.ts +++ b/src/typescript/sdk/src/client/emojicoin-client.ts @@ -141,7 +141,7 @@ export class EmojicoinClient { minOutputAmount?: bigint | number; }) { const { - aptos = getAptosClient().aptos, + aptos = getAptosClient(), integrator = INTEGRATOR_ADDRESS, integratorFeeRateBPs = 0, minOutputAmount = 1n, diff --git a/src/typescript/sdk/src/const.ts b/src/typescript/sdk/src/const.ts index ca4564672..fcd7f267e 100644 --- a/src/typescript/sdk/src/const.ts +++ b/src/typescript/sdk/src/const.ts @@ -40,7 +40,7 @@ if (!APTOS_NETWORK) { throw new Error(`Invalid network: ${network}`); } -const allAPIKeys: Record = { +const clientKeys: Record = { [Network.LOCAL]: process.env.NEXT_PUBLIC_LOCAL_APTOS_API_KEY, [Network.CUSTOM]: process.env.NEXT_PUBLIC_CUSTOM_APTOS_API_KEY, [Network.DEVNET]: process.env.NEXT_PUBLIC_DEVNET_APTOS_API_KEY, @@ -48,21 +48,22 @@ const allAPIKeys: Record = { [Network.MAINNET]: process.env.NEXT_PUBLIC_MAINNET_APTOS_API_KEY, }; -const apiKey = allAPIKeys[APTOS_NETWORK]; -if (typeof apiKey === "undefined") { - // Do nothing if we're on a local network, because we don't need an API key for it. - if (APTOS_NETWORK !== "local") { - if (APTOS_NETWORK === "custom") { - console.warn(`No API key set. Ignoring because we're on the \`${APTOS_NETWORK}\` network.`); - } else { - throw new Error(`Invalid API key set for the network: ${APTOS_NETWORK}: ${apiKey}`); - } - } -} +const serverKeys: Record = { + [Network.LOCAL]: process.env.SERVER_LOCAL_APTOS_API_KEY, + [Network.CUSTOM]: process.env.SERVER_CUSTOM_APTOS_API_KEY, + [Network.DEVNET]: process.env.SERVER_DEVNET_APTOS_API_KEY, + [Network.TESTNET]: process.env.SERVER_TESTNET_APTOS_API_KEY, + [Network.MAINNET]: process.env.SERVER_MAINNET_APTOS_API_KEY, +}; + +const clientApiKey = clientKeys[APTOS_NETWORK]; +const serverApiKey = serverKeys[APTOS_NETWORK]; + +export const getAptosApiKey = () => serverApiKey ?? clientApiKey; + // Select the API key from the list of env API keys. This means we don't have to change the env // var for API keys when changing environments- we just need to provide them all every time, which // is much simpler. -export const APTOS_API_KEY = apiKey; export const MODULE_ADDRESS = (() => AccountAddress.from(process.env.NEXT_PUBLIC_MODULE_ADDRESS))(); export const REWARDS_MODULE_ADDRESS = (() => AccountAddress.from(process.env.NEXT_PUBLIC_REWARDS_MODULE_ADDRESS))(); diff --git a/src/typescript/sdk/src/emojicoin_dot_fun/aptos-framework.ts b/src/typescript/sdk/src/emojicoin_dot_fun/aptos-framework.ts index 61e246e5b..9a2b87ee4 100644 --- a/src/typescript/sdk/src/emojicoin_dot_fun/aptos-framework.ts +++ b/src/typescript/sdk/src/emojicoin_dot_fun/aptos-framework.ts @@ -90,7 +90,7 @@ export class Mint extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const { aptos } = getAptosClient(aptosConfig); + const aptos = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -190,7 +190,7 @@ export class BatchTransferCoins extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const { aptos } = getAptosClient(aptosConfig); + const aptos = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -291,7 +291,7 @@ export class TransferCoins extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const { aptos } = getAptosClient(aptosConfig); + const aptos = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } diff --git a/src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts b/src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts index 69ad70d93..c37a9b73f 100644 --- a/src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts +++ b/src/typescript/sdk/src/emojicoin_dot_fun/emojicoin-dot-fun.ts @@ -113,7 +113,7 @@ export class Chat extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const { aptos } = getAptosClient(aptosConfig); + const aptos = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -220,7 +220,7 @@ export class ProvideLiquidity extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const { aptos } = getAptosClient(aptosConfig); + const aptos = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -308,7 +308,7 @@ export class RegisterMarket extends EntryFunctionPayloadBuilder { }): Promise<{ data: { amount: number; unitPrice: number }; error: boolean }> { const { aptosConfig } = args; - const { aptos } = getAptosClient(aptosConfig); + const aptos = getAptosClient(aptosConfig); const rawTransaction = await this.builder({ ...args, integrator: AccountAddress.ONE, @@ -348,7 +348,7 @@ export class RegisterMarket extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const { aptos } = getAptosClient(aptosConfig); + const aptos = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -453,7 +453,7 @@ export class RemoveLiquidity extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const { aptos } = getAptosClient(aptosConfig); + const aptos = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -585,7 +585,7 @@ export class Swap extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const { aptos } = getAptosClient(aptosConfig); + const aptos = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } @@ -634,7 +634,7 @@ export class Swap extends EntryFunctionPayloadBuilder { }): Promise { const { aptosConfig } = args; - const { aptos } = getAptosClient(aptosConfig); + const aptos = getAptosClient(aptosConfig); const rawTransaction = await this.builder({ ...args, integrator: AccountAddress.ONE, @@ -734,7 +734,7 @@ export class SwapWithRewards extends EntryFunctionPayloadBuilder { options, feePayerAddress: feePayer, }); - const { aptos } = getAptosClient(aptosConfig); + const aptos = getAptosClient(aptosConfig); return new EntryFunctionTransactionBuilder(payloadBuilder, aptos, rawTransactionInput); } diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/home.ts b/src/typescript/sdk/src/indexer-v2/queries/app/home.ts index 227db8ef5..c99f819d0 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/app/home.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/app/home.ts @@ -74,7 +74,7 @@ export const fetchFeaturedMarket = async () => * @returns The number of registered markets at the latest processed transaction version */ export const fetchNumRegisteredMarkets = async () => { - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); let latestVersion: bigint; try { latestVersion = await getLatestProcessedEmojicoinVersion(); diff --git a/src/typescript/sdk/src/indexer-v2/queries/utils.ts b/src/typescript/sdk/src/indexer-v2/queries/utils.ts index 775f0c019..5a2212df4 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/utils.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/utils.ts @@ -176,7 +176,9 @@ export function queryHelperWithCount< >( queryFn: QueryFunction, QueryArgs>, convert: (rows: Row) => OutputType -): (args: WithConfig) => Promise<{ rows: OutputType[]; count: number | null }> { +): ( + args: WithConfig +) => Promise<{ rows: OutputType[]; count: number | null; error: unknown }> { const query = async (args: WithConfig) => { const { minimumVersion, ...queryArgs } = args; const innerQuery = queryFn(queryArgs as QueryArgs); @@ -192,10 +194,10 @@ export function queryHelperWithCount< throw new Error(JSON.stringify(res)); } const rows = extractRows(res); - return { rows: rows.map(convert), count: res.count }; + return { rows: rows.map(convert), count: res.count, error: res.error }; } catch (e) { console.error(e); - return { rows: [], count: null }; + return { rows: [], count: null, error: e }; } }; diff --git a/src/typescript/sdk/src/utils/aptos-client.ts b/src/typescript/sdk/src/utils/aptos-client.ts index e493d89af..6ab8c64d6 100644 --- a/src/typescript/sdk/src/utils/aptos-client.ts +++ b/src/typescript/sdk/src/utils/aptos-client.ts @@ -1,23 +1,48 @@ -import { Aptos, AptosConfig, type ClientConfig } from "@aptos-labs/ts-sdk"; -import { APTOS_API_KEY, APTOS_NETWORK } from "../const"; +import { + Aptos, + AptosConfig, + NetworkToFaucetAPI, + NetworkToIndexerAPI, + NetworkToNetworkName, + NetworkToNodeAPI, + type ClientConfig, +} from "@aptos-labs/ts-sdk"; +import { APTOS_NETWORK, getAptosApiKey } from "../const"; export const APTOS_CONFIG: Partial = { - API_KEY: APTOS_API_KEY, + API_KEY: getAptosApiKey(), }; -export function getAptosClient(additionalConfig?: Partial): { - aptos: Aptos; - config: AptosConfig; -} { +const toDockerUrl = (url: string) => url.replace("127.0.0.1", "host.docker.internal"); + +export function getAptosClient(additionalConfig?: Partial): Aptos { const network = APTOS_NETWORK; + + if (network === "local" && typeof window === "undefined") { + /* eslint-disable-next-line @typescript-eslint/no-var-requires */ + const fs = require("node:fs"); + if (fs.existsSync("/.dockerenv")) { + const config = new AptosConfig({ + network: NetworkToNetworkName["local"], + fullnode: toDockerUrl(NetworkToNodeAPI["local"]), + indexer: toDockerUrl(NetworkToIndexerAPI["local"]), + faucet: toDockerUrl(NetworkToFaucetAPI["local"]), + clientConfig: { + ...APTOS_CONFIG, + ...additionalConfig?.clientConfig, + }, + }); + return new Aptos(config); + } + } + const config = new AptosConfig({ network, ...additionalConfig, clientConfig: { - ...additionalConfig?.clientConfig, ...APTOS_CONFIG, + ...additionalConfig?.clientConfig, }, }); - const aptos = new Aptos(config); - return { aptos, config }; + return new Aptos(config); } diff --git a/src/typescript/sdk/src/utils/test/get-publish-txn-from-indexer.ts b/src/typescript/sdk/src/utils/test/get-publish-txn-from-indexer.ts index 635c6eb6c..186a8f5a8 100644 --- a/src/typescript/sdk/src/utils/test/get-publish-txn-from-indexer.ts +++ b/src/typescript/sdk/src/utils/test/get-publish-txn-from-indexer.ts @@ -9,7 +9,7 @@ import { getAptosClient } from "../aptos-client"; import { getPublisherPrivateKey } from "./helpers"; export const getPublishTransactionFromIndexer = async () => { - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); const publisher = Account.fromPrivateKey({ privateKey: getPublisherPrivateKey(), }); diff --git a/src/typescript/sdk/src/utils/test/helpers.ts b/src/typescript/sdk/src/utils/test/helpers.ts index 43d577f1c..81f3261f4 100644 --- a/src/typescript/sdk/src/utils/test/helpers.ts +++ b/src/typescript/sdk/src/utils/test/helpers.ts @@ -53,7 +53,7 @@ export function getPublishHelpers() { ); } - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); const privateKeyString = process.env.PUBLISHER_PRIVATE_KEY; if (!privateKeyString) { diff --git a/src/typescript/sdk/src/utils/test/publish.ts b/src/typescript/sdk/src/utils/test/publish.ts index d7a628d54..f1f0776df 100644 --- a/src/typescript/sdk/src/utils/test/publish.ts +++ b/src/typescript/sdk/src/utils/test/publish.ts @@ -120,7 +120,7 @@ function extractJsonFromText(originalCommand: string, text: string): ResultJSON } export async function publishForTest(privateKeyString: string) { - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); const publisher = Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey(Hex.fromHexString(privateKeyString).toUint8Array()), }); @@ -157,7 +157,7 @@ export async function getModuleExists( publisherAddress: AccountAddressInput, moduleName: string ): Promise { - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); const abiExists = typeof ( await aptos.account.getAccountModule({ diff --git a/src/typescript/sdk/tests/e2e/broker/websockets.test.ts b/src/typescript/sdk/tests/e2e/broker/websockets.test.ts index 783903cb3..efe7b8906 100644 --- a/src/typescript/sdk/tests/e2e/broker/websockets.test.ts +++ b/src/typescript/sdk/tests/e2e/broker/websockets.test.ts @@ -49,7 +49,7 @@ const customWaitFor = async (condition: () => boolean) => describe("tests to ensure that websocket event subscriptions work as expected", () => { const registrants = getFundedAccounts("040", "041", "042", "043", "044", "045"); - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); const senderArgs = Array.from(registrants).map((acc) => ({ registrant: acc, provider: acc, diff --git a/src/typescript/sdk/tests/e2e/fund.test.ts b/src/typescript/sdk/tests/e2e/fund.test.ts index 0f874acf5..37f6bcef0 100644 --- a/src/typescript/sdk/tests/e2e/fund.test.ts +++ b/src/typescript/sdk/tests/e2e/fund.test.ts @@ -5,7 +5,7 @@ import { getAptosClient } from "../../src/utils/test"; jest.setTimeout(10000); describe("tests a simple faucet fund account request", () => { - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); const account = Ed25519Account.generate(); beforeAll(async () => { diff --git a/src/typescript/sdk/tests/e2e/queries/address.test.ts b/src/typescript/sdk/tests/e2e/queries/address.test.ts index 787befb85..88801ad73 100644 --- a/src/typescript/sdk/tests/e2e/queries/address.test.ts +++ b/src/typescript/sdk/tests/e2e/queries/address.test.ts @@ -11,7 +11,7 @@ jest.setTimeout(20000); describe("address standardization tests", () => { it("standardizes a user's address", async () => { const user = getFundedAccount("005"); - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); const { marketAddress, emojicoin, emojicoinLP, emojis } = await TestHelpers.registerRandomMarket({ registrant: user }); diff --git a/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts b/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts index a73fa918f..d6c726166 100644 --- a/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts +++ b/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts @@ -1,5 +1,4 @@ import { - APTOS_API_KEY, APTOS_NETWORK, getEmojicoinMarketAddressAndTypeTags, INTEGRATOR_ADDRESS, @@ -22,7 +21,6 @@ import { Network, } from "@aptos-labs/ts-sdk"; import { EXACT_TRANSITION_INPUT_AMOUNT } from "../../../../src/utils/test/helpers"; -import { getAptosClient } from "../../../../src/utils/aptos-client"; import { calculatePeriodBoundariesCrossed } from "../../../../src/utils/test"; jest.setTimeout(15000); @@ -105,26 +103,6 @@ describe("all submission types for the emojicoin client", () => { expect(emojicoinClient.aptos.config.network).toEqual(Network.TESTNET); }); - it("sets the API key in the aptos client configuration", () => { - const config = new AptosConfig({ - network: Network.TESTNET, - }); - const aptos = new Aptos(config); - const emojicoinClient = new EmojicoinClient({ aptos }); - expect(aptos.config.clientConfig?.API_KEY).toEqual(APTOS_API_KEY); - expect(emojicoinClient.aptos.config.clientConfig?.API_KEY).toEqual(APTOS_API_KEY); - }); - - it("sets the API key in the client returned by getAptos()", () => { - const config = new AptosConfig({ - network: Network.TESTNET, - }); - const { aptos } = getAptosClient(config); - const emojicoinClient = new EmojicoinClient({ aptos }); - expect(aptos.config.clientConfig?.API_KEY).toEqual(APTOS_API_KEY); - expect(emojicoinClient.aptos.config.clientConfig?.API_KEY).toEqual(APTOS_API_KEY); - }); - it("creates the aptos client with the correct default configuration settings", () => { expect(emojicoin.aptos.config.network).toEqual(process.env.NEXT_PUBLIC_APTOS_NETWORK); expect(emojicoin.aptos.config.network).toEqual(APTOS_NETWORK); diff --git a/src/typescript/sdk/tests/e2e/queries/market-state.test.ts b/src/typescript/sdk/tests/e2e/queries/market-state.test.ts index 989c200dc..721826a5d 100644 --- a/src/typescript/sdk/tests/e2e/queries/market-state.test.ts +++ b/src/typescript/sdk/tests/e2e/queries/market-state.test.ts @@ -11,7 +11,7 @@ import { type JsonValue } from "../../../src/types/json-types"; jest.setTimeout(20000); describe("queries a market by market state", () => { - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); const registrant = getFundedAccount("037"); it("fetches the market state for a market based on an emoji symbols array", async () => { diff --git a/src/typescript/sdk/tests/e2e/queries/num-markets.test.ts b/src/typescript/sdk/tests/e2e/queries/num-markets.test.ts index 63032dc26..3f5884efc 100644 --- a/src/typescript/sdk/tests/e2e/queries/num-markets.test.ts +++ b/src/typescript/sdk/tests/e2e/queries/num-markets.test.ts @@ -8,7 +8,7 @@ import { getFundedAccounts } from "../../../src/utils/test/test-accounts"; jest.setTimeout(20000); describe("fetches the number of registered markets based on the latest processed version", () => { - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); const registrants = getFundedAccounts("031", "032", "033", "034", "035", "036"); let versionsAndNumMarkets: { version: bigint; numMarkets: bigint }[]; diff --git a/src/typescript/sdk/tests/e2e/queries/simple.test.ts b/src/typescript/sdk/tests/e2e/queries/simple.test.ts index 978996a54..6ab776b5e 100644 --- a/src/typescript/sdk/tests/e2e/queries/simple.test.ts +++ b/src/typescript/sdk/tests/e2e/queries/simple.test.ts @@ -17,7 +17,7 @@ import { fetchLatestStateEventForMarket, fetchLiquidityEvents } from "."; jest.setTimeout(20000); describe("queries swap_events and returns accurate swap row data", () => { - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); const [registrant, user, swapper, provider] = getFundedAccounts("007", "008", "009", "010"); const marketEmojiNames: SymbolEmojiName[][] = [ ["scroll"], diff --git a/src/typescript/sdk/tests/e2e/queries/sorted/sort-queries.test.ts b/src/typescript/sdk/tests/e2e/queries/sorted/sort-queries.test.ts index 3dfbc49ec..ff793143c 100644 --- a/src/typescript/sdk/tests/e2e/queries/sorted/sort-queries.test.ts +++ b/src/typescript/sdk/tests/e2e/queries/sorted/sort-queries.test.ts @@ -27,7 +27,7 @@ import { jest.setTimeout(20000); describe("sorting queries for the sort filters on the home page", () => { - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); const registrants = getFundedAccounts("023", "024", "025", "026", "027", "028", "029", "030"); let latestTransactionVersion: number; diff --git a/src/typescript/sdk/tests/e2e/queries/volume.test.ts b/src/typescript/sdk/tests/e2e/queries/volume.test.ts index 666ee6d66..465cf5ded 100644 --- a/src/typescript/sdk/tests/e2e/queries/volume.test.ts +++ b/src/typescript/sdk/tests/e2e/queries/volume.test.ts @@ -38,7 +38,7 @@ const TWENTY_FIVE_SECONDS = 20 * 1000; const TWO_SECONDS = 2000; describe("queries swap_events and returns accurate swap row data", () => { - const { aptos } = getAptosClient(); + const aptos = getAptosClient(); const fundedAccounts = getFundedAccounts("011", "012", "013", "014", "015", "016"); it("sums a market's daily volume over multiple 1-minute periods with 3 swaps", async () => { From c36f3e7c75ef4dbcccb7365e35e92e7c67066c27 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:47:43 -0800 Subject: [PATCH 72/94] [ECO-2488] Fix emojicoin balance query by using the view function instead of the external indexer API (#404) --- .../lib/hooks/queries/use-wallet-balance.ts | 12 ++--- .../src/emojicoin_dot_fun/aptos-framework.ts | 54 ++++++++++++++++++- .../sdk/src/emojicoin_dot_fun/index.ts | 1 + 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/typescript/frontend/src/lib/hooks/queries/use-wallet-balance.ts b/src/typescript/frontend/src/lib/hooks/queries/use-wallet-balance.ts index 175617aa9..e969218d4 100644 --- a/src/typescript/frontend/src/lib/hooks/queries/use-wallet-balance.ts +++ b/src/typescript/frontend/src/lib/hooks/queries/use-wallet-balance.ts @@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import { withResponseError } from "./client"; import { useCallback, useMemo, useRef, useState } from "react"; import { type TypeTagInput } from "@sdk/emojicoin_dot_fun"; +import { Balance } from "@sdk/emojicoin_dot_fun/aptos-framework"; /** * __NOTE: If you're using this for a connected user's APT balance, you should use__ @@ -60,12 +61,11 @@ export const useWalletBalance = ({ return res; } return withResponseError( - aptos - .getAccountCoinAmount({ - accountAddress, - coinType: coinType.toString() as `${string}::${string}::${string}`, - }) - .then((r) => BigInt(r)) + Balance.view({ + aptos, + owner: accountAddress, + typeTags: [coinType], + }).then((res) => BigInt(res)) ); }, placeholderData: (previousBalance) => previousBalance ?? 0, diff --git a/src/typescript/sdk/src/emojicoin_dot_fun/aptos-framework.ts b/src/typescript/sdk/src/emojicoin_dot_fun/aptos-framework.ts index 9a2b87ee4..f069dc6f8 100644 --- a/src/typescript/sdk/src/emojicoin_dot_fun/aptos-framework.ts +++ b/src/typescript/sdk/src/emojicoin_dot_fun/aptos-framework.ts @@ -2,13 +2,13 @@ import { MoveVector, AccountAddress, + type Aptos, U64, type AccountAddressInput, type Uint64, type AptosConfig, type InputGenerateTransactionOptions, buildTransaction, - type Aptos, type Account, type WaitForTransactionOptions, type UserTransactionResponse, @@ -21,7 +21,7 @@ import { EntryFunctionTransactionBuilder, ViewFunctionPayloadBuilder, } from "./payload-builders"; -import { type TypeTagInput } from "."; +import { type Uint64String, type TypeTagInput } from "."; import { getAptosClient } from "../utils/aptos-client"; export type MintPayloadMoveArguments = { @@ -365,3 +365,53 @@ export class ExistsAt extends ViewFunctionPayloadBuilder<[boolean]> { return res; } } + +export type BalancePayloadMoveArguments = { + owner: AccountAddress; +}; + +/** + *``` + * #[view] + * public fun balance( + * owner: address, + * ): u64 + *``` + * */ + +export class Balance extends ViewFunctionPayloadBuilder<[Uint64String]> { + public readonly moduleAddress = AccountAddress.ONE; + + public readonly moduleName = "coin"; + + public readonly functionName = "balance"; + + public readonly args: BalancePayloadMoveArguments; + + public readonly typeTags: [TypeTag]; // [CoinType] + + constructor(args: { + owner: AccountAddressInput; // address + typeTags: [TypeTagInput]; // [CoinType] + }) { + super(); + const { owner, typeTags } = args; + + this.args = { + owner: AccountAddress.from(owner), + }; + this.typeTags = typeTags.map((typeTag) => + typeof typeTag === "string" ? parseTypeTag(typeTag) : typeTag + ) as [TypeTag]; + } + + static async view(args: { + aptos: Aptos | AptosConfig; + owner: AccountAddressInput; // address + typeTags: [TypeTagInput]; // [CoinType] + options?: LedgerVersionArg; + }): Promise { + const [res] = await new Balance(args).view(args); + return res; + } +} diff --git a/src/typescript/sdk/src/emojicoin_dot_fun/index.ts b/src/typescript/sdk/src/emojicoin_dot_fun/index.ts index cf822b613..4525fb057 100644 --- a/src/typescript/sdk/src/emojicoin_dot_fun/index.ts +++ b/src/typescript/sdk/src/emojicoin_dot_fun/index.ts @@ -1,3 +1,4 @@ +export * as AptosFramework from "./aptos-framework"; export * as EmojicoinDotFun from "./emojicoin-dot-fun"; export * from "./types"; export * from "./utils"; From 7fc29c141434e42a611b526dbdbb960b3e96be9b Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:57:47 -0800 Subject: [PATCH 73/94] [ECO-2487] Fix 24h volume, update text to say recent volume when on bump order (#403) --- .../home/components/emoji-table/utils.ts | 2 +- .../home/components/table-card/TableCard.tsx | 59 +++++++++++-------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/typescript/frontend/src/components/pages/home/components/emoji-table/utils.ts b/src/typescript/frontend/src/components/pages/home/components/emoji-table/utils.ts index 7ba2bf7e9..bd3c385b3 100644 --- a/src/typescript/frontend/src/components/pages/home/components/emoji-table/utils.ts +++ b/src/typescript/frontend/src/components/pages/home/components/emoji-table/utils.ts @@ -47,7 +47,7 @@ export const stateEventsToProps = ( symbol, emojis, marketID: Number(marketID), - staticMarketCap: (marketCap ?? 0).toString(), + staticMarketCap: marketCap.toString(), staticVolume24H: (volume24H ?? 0).toString(), trigger: e.market.trigger, searchEmojisKey: toSearchEmojisKey(searchEmojis), diff --git a/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx b/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx index 9110d977d..2c9e351da 100644 --- a/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/table-card/TableCard.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react"; import { translationFunction } from "context/language-context"; import { Column, Flex } from "@containers"; import { Text } from "components/text"; @@ -9,7 +9,6 @@ import { emojisToName } from "lib/utils/emojis-to-name-or-symbol"; import { useEventStore, useUserSettings } from "context/event-store-context"; import { motion, type MotionProps, useAnimationControls, useMotionValue } from "framer-motion"; import { Arrow } from "components/svg"; -import Big from "big.js"; import { toCoinDecimalString } from "lib/utils/decimals"; import { borderVariants, @@ -27,10 +26,11 @@ import { tableCardVariants, } from "./animation-variants/grid-variants"; import LinkOrAnimationTrigger from "./LinkOrAnimationTrigger"; -import { isMarketStateModel } from "@sdk/indexer-v2/types"; -import "./module.css"; import { type SymbolEmojiData } from "@sdk/emoji_data"; import { Emoji } from "utils/emoji"; +import { SortMarketsBy } from "@sdk/indexer-v2/types/common"; +import Big from "big.js"; +import "./module.css"; const getFontSize = (emojis: SymbolEmojiData[]) => emojis.length <= 2 ? ("pixel-heading-1" as const) : ("pixel-heading-1b" as const); @@ -53,11 +53,28 @@ const TableCard = ({ const controls = useAnimationControls(); const animationsOn = useUserSettings((s) => s.animate); - const [marketCap, setMarketCap] = useState(Big(staticMarketCap)); - const [dailyVolume, setDailyVolume] = useState(Big(staticVolume24H)); - const animations = useEventStore( + const stateEvents = useEventStore( (s) => s.getMarket(emojis.map((e) => e.emoji))?.stateEvents ?? [] ); + const animationEvent = stateEvents.at(0); + + const { isBumpOrder, secondaryMetric, marketCap } = useMemo(() => { + const isBumpOrder = sortBy === SortMarketsBy.BumpOrder; + const { lastSwapVolume, marketCap } = animationEvent + ? { + lastSwapVolume: Big(animationEvent.lastSwap.quoteVolume.toString()), + marketCap: animationEvent.state.instantaneousStats.marketCap, + } + : { + lastSwapVolume: 0, + marketCap: staticMarketCap, + }; + return { + isBumpOrder, + secondaryMetric: isBumpOrder ? lastSwapVolume : Big(staticVolume24H), + marketCap, + }; + }, [sortBy, animationEvent, staticVolume24H, staticMarketCap]); // Keep track of whether or not the component is mounted to avoid animating an unmounted component. useLayoutEffect(() => { @@ -77,7 +94,9 @@ const TableCard = ({ controls.stop(); if (isMounted.current) { controls.start(variant).then(() => { - controls.start("initial"); + if (isMounted.current) { + controls.start("initial"); + } }); } } @@ -86,29 +105,17 @@ const TableCard = ({ ); useEffect(() => { - if (animations && animations.length) { - const event = animations.at(0); - if (!event) { - setDailyVolume(Big(0)); - setMarketCap(Big(0)); - } else { - setMarketCap(Big(event.state.instantaneousStats.marketCap.toString())); - if (isMarketStateModel(event)) { - setDailyVolume(Big(event.dailyVolume.toString())); - } - runAnimationSequence(event); - } + if (animationEvent) { + runAnimationSequence(animationEvent); } - - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [animations, runAnimationSequence]); + }, [animationEvent, runAnimationSequence, isBumpOrder]); const { ref: marketCapRef } = useLabelScrambler( toCoinDecimalString(marketCap.toString(), 2), " APT" ); const { ref: dailyVolumeRef } = useLabelScrambler( - toCoinDecimalString(dailyVolume.toString(), 2), + toCoinDecimalString(secondaryMetric.toString(), 2), " APT" ); @@ -274,7 +281,7 @@ const TableCard = ({ "group-hover:text-ec-blue uppercase p-[1px] transition-all" } > - {t("24h Volume")} + {isBumpOrder ? t("Last Swap") : t("24h Volume")}
- {toCoinDecimalString(dailyVolume.toString(), 2) + " APT"} + {toCoinDecimalString(secondaryMetric.toString(), 2) + " APT"} From c0e726ff51ffec0ae98573c3ed981a554b9fe59c Mon Sep 17 00:00:00 2001 From: alnoki <43892045+alnoki@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:07:46 -0800 Subject: [PATCH 74/94] [ECO-2205] Refactor indexer scaling, update README (#407) --- src/cloud-formation/README.md | 6 ++++++ .../deploy-indexer-production.yaml | 3 +++ src/cloud-formation/indexer.cfn.yaml | 20 ++++++++++++++----- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/cloud-formation/README.md b/src/cloud-formation/README.md index 3471c27da..aea6e7d49 100644 --- a/src/cloud-formation/README.md +++ b/src/cloud-formation/README.md @@ -41,6 +41,10 @@ indexer deployments. ## Setup +1. Choose an [AWS region][aws regions] like `us-east-2` (Ohio) if you are going + to get indexer data from the [Aptos Labs gRPC endpoint], which is located in + [GCP region][gcp regions] `us-central1` (Iowa). + 1. [Make Route 53 the DNS service for a domain you own], which will automatically be configured with a subdomain for each deployment environment. @@ -494,6 +498,7 @@ REST and WebSocket endpoints. [auto-selection of aurora az]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html#cfn-rds-dbinstance-availabilityzone [availability zone]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones [aws cloudformation]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html +[aws regions]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions [aws routes docs]: https://docs.aws.amazon.com/vpc/latest/userguide/subnet-route-tables.html#route-table-routes [aws-recommended vpc cidr block]: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-cidr-blocks.html [az-specific nat gateways]: https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-basics.html @@ -513,6 +518,7 @@ REST and WebSocket endpoints. [ecr pull through cache rule creation docs]: https://docs.aws.amazon.com/AmazonECR/latest/userguide/pull-through-cache-creating-rule.html [ecs task execution iam role]: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html [fault tolerant replica promotion]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.AuroraHighAvailability.html#Aurora.Managing.FaultTolerance +[gcp regions]: https://cloud.google.com/compute/docs/regions-zones#available [gitsync]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/git-sync.html [gitsync event]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/git-sync-status.html#git-sync-status-sync-events [gitsync iam role]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/git-sync-prereq.html#git-sync-prereq-iam diff --git a/src/cloud-formation/deploy-indexer-production.yaml b/src/cloud-formation/deploy-indexer-production.yaml index 4c1b2c338..c3295933a 100644 --- a/src/cloud-formation/deploy-indexer-production.yaml +++ b/src/cloud-formation/deploy-indexer-production.yaml @@ -1,6 +1,9 @@ --- parameters: + BrokerCpu: 1024 BrokerImageVersion: '1.0.1' + BrokerMemory: 2048 + DbMaxCapacity: 8 DeployAlb: 'true' DeployAlbDnsRecord: 'true' DeployBastionHost: 'true' diff --git a/src/cloud-formation/indexer.cfn.yaml b/src/cloud-formation/indexer.cfn.yaml index d4adc4c0b..181d7fe4e 100644 --- a/src/cloud-formation/indexer.cfn.yaml +++ b/src/cloud-formation/indexer.cfn.yaml @@ -107,10 +107,16 @@ Parameters: BastionHostAmiId: Default: 'ami-030cb86c12b18e236' Type: 'AWS::EC2::Image::Id' + BrokerCpu: + Default: 256 + Type: 'Number' BrokerImageVersion: Type: 'String' + BrokerMemory: + Default: 512 + Type: 'Number' DbMaxCapacity: - Default: 16 + Default: 1 Type: 'Number' DbMinCapacity: Default: 0.5 @@ -654,10 +660,10 @@ Resources: - 'Constants' - 'Networking' - 'BrokerPort' - Cpu: '1024' + Cpu: !Ref 'BrokerCpu' ExecutionRoleArn: !GetAtt 'ContainerRole.Arn' Family: !Ref 'AWS::StackName' - Memory: '2048' + Memory: !Ref 'BrokerMemory' NetworkMode: 'awsvpc' RequiresCompatibilities: - 'FARGATE' @@ -676,6 +682,10 @@ Resources: # Cluster for running ECS containers. ContainerCluster: Condition: 'DeployContainers' + Properties: + ClusterSettings: + - Name: 'containerInsights' + Value: 'enabled' Type: 'AWS::ECS::Cluster' # Log group for ECS task logging. ContainerLogGroup: @@ -1305,10 +1315,10 @@ Resources: - 'Constants' - 'Networking' - 'PostgrestHealthCheckPort' - Cpu: '1024' + Cpu: '256' ExecutionRoleArn: !GetAtt 'ContainerRole.Arn' Family: !Ref 'AWS::StackName' - Memory: '2048' + Memory: '512' NetworkMode: 'awsvpc' RequiresCompatibilities: - 'FARGATE' From 295cf611950f66651452baa3e6ad6d6aef583f9b Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Fri, 22 Nov 2024 20:47:59 +0100 Subject: [PATCH 75/94] [ECO-2483] Cache VPN queries (#402) Co-authored-by: Matt <90358481+xbtmatt@users.noreply.github.com> --- src/typescript/frontend/src/configs/index.ts | 1 - .../src/configs/local-storage-keys.ts | 42 ++++++++++++++++++- .../src/context/language-context/helpers.ts | 5 ++- .../src/context/theme-context/index.tsx | 8 ++-- .../src/hooks/use-is-user-geoblocked.ts | 12 +++++- src/typescript/frontend/src/utils/index.ts | 5 ++- 6 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/typescript/frontend/src/configs/index.ts b/src/typescript/frontend/src/configs/index.ts index c9e178638..6f2955f5c 100644 --- a/src/typescript/frontend/src/configs/index.ts +++ b/src/typescript/frontend/src/configs/index.ts @@ -1,3 +1,2 @@ -export { default as LOCAL_STORAGE_KEYS } from "./local-storage-keys"; export { EN, languages, languageList } from "./languages"; export { default as REGEX } from "./regex"; diff --git a/src/typescript/frontend/src/configs/local-storage-keys.ts b/src/typescript/frontend/src/configs/local-storage-keys.ts index fc728ef83..e36b0c3ff 100644 --- a/src/typescript/frontend/src/configs/local-storage-keys.ts +++ b/src/typescript/frontend/src/configs/local-storage-keys.ts @@ -1,8 +1,48 @@ +import { parseJSON, stringifyJSON } from "utils"; import packages from "../../package.json"; const LOCAL_STORAGE_KEYS = { theme: `${packages.name}_theme`, language: `${packages.name}_language`, + geoblocking: `${packages.name}_geoblocking`, }; -export default LOCAL_STORAGE_KEYS; +const LOCAL_STORAGE_CACHE_TIME = { + theme: Infinity, + language: Infinity, + geoblocking: 7 * 24 * 60 * 60 * 1000, // 7 days. +}; + +export type LocalStorageCache = { + expiry: number; + data: T | null; +}; + +/** + * Note that this data is not validated and any change in data type returned from this function + * should be validated to ensure that persisted cache data between multiple builds can cause errors + * with unexpected data types. + */ +export function readLocalStorageCache(key: keyof typeof LOCAL_STORAGE_KEYS): T | null { + const str = localStorage.getItem(key); + if (str === null) { + return null; + } + const cache = parseJSON>(str); + try { + if (new Date(cache.expiry) > new Date()) { + return cache.data; + } + } catch (e) { + return null; + } + return null; +} + +export function writeLocalStorageCache(key: keyof typeof LOCAL_STORAGE_KEYS, data: T) { + const cache: LocalStorageCache = { + expiry: new Date().getTime() + LOCAL_STORAGE_CACHE_TIME[key], + data, + }; + localStorage.setItem(key, stringifyJSON>(cache)); +} diff --git a/src/typescript/frontend/src/context/language-context/helpers.ts b/src/typescript/frontend/src/context/language-context/helpers.ts index 3c32798ba..a2f03c293 100644 --- a/src/typescript/frontend/src/context/language-context/helpers.ts +++ b/src/typescript/frontend/src/context/language-context/helpers.ts @@ -1,10 +1,11 @@ -import { EN, LOCAL_STORAGE_KEYS, REGEX } from "configs"; +import { EN, REGEX } from "configs"; +import { readLocalStorageCache } from "configs/local-storage-keys"; export const getLanguageCodeFromLocalStorage = () => { if (typeof window === "undefined") { return EN.locale; } - return localStorage.getItem(LOCAL_STORAGE_KEYS.language) ?? EN.locale; + return readLocalStorageCache("language") ?? EN.locale; }; export const translatedTextIncludesVariable = (translatedText: string) => { diff --git a/src/typescript/frontend/src/context/theme-context/index.tsx b/src/typescript/frontend/src/context/theme-context/index.tsx index 0f049a704..a083b2c42 100644 --- a/src/typescript/frontend/src/context/theme-context/index.tsx +++ b/src/typescript/frontend/src/context/theme-context/index.tsx @@ -6,7 +6,7 @@ import { type DefaultTheme } from "styled-components"; import dark from "theme/dark"; import light from "theme/light"; -import { LOCAL_STORAGE_KEYS } from "configs"; +import { readLocalStorageCache, writeLocalStorageCache } from "configs/local-storage-keys"; type ContextType = { theme: DefaultTheme; @@ -28,7 +28,7 @@ const ThemeContextProvider: React.FC> = ({ children }) => const [theme, setTheme] = useState(() => { const themeFromStorage = getThemeValueFromLS(); - localStorage.setItem(LOCAL_STORAGE_KEYS.theme, themeFromStorage); + writeLocalStorageCache("theme", themeFromStorage); return { theme: themeValues[themeFromStorage], key: themeFromStorage }; }); @@ -42,12 +42,12 @@ const ThemeContextProvider: React.FC> = ({ children }) => const themeFromStorage = getThemeValueFromLS(); const newValue = themeFromStorage === LIGHT ? DARK : LIGHT; - localStorage.setItem(LOCAL_STORAGE_KEYS.theme, newValue); + writeLocalStorageCache("theme", newValue); setTheme({ theme: themeValues[newValue], key: newValue }); } function getThemeValueFromLS() { - let themeFromStorage = localStorage.getItem(LOCAL_STORAGE_KEYS.theme) ?? LIGHT; + let themeFromStorage = readLocalStorageCache("theme") ?? LIGHT; if (!(themeFromStorage in themeValues)) { themeFromStorage = LIGHT; diff --git a/src/typescript/frontend/src/hooks/use-is-user-geoblocked.ts b/src/typescript/frontend/src/hooks/use-is-user-geoblocked.ts index c8882d86c..b887d1146 100644 --- a/src/typescript/frontend/src/hooks/use-is-user-geoblocked.ts +++ b/src/typescript/frontend/src/hooks/use-is-user-geoblocked.ts @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { MS_IN_ONE_DAY } from "components/charts/const"; +import { readLocalStorageCache, writeLocalStorageCache } from "configs/local-storage-keys"; import { isUserGeoblocked } from "utils/geolocation"; const SEVEN_DAYS_MS = 7 * MS_IN_ONE_DAY; @@ -9,7 +10,16 @@ const useIsUserGeoblocked = (args?: { explicitlyGeoblocked: boolean }) => { const { explicitlyGeoblocked = false } = args ?? {}; const { data } = useQuery({ queryKey: ["geoblocked"], - queryFn: () => isUserGeoblocked(), + queryFn: async () => { + let geoblocked = readLocalStorageCache("geoblocking"); + + if (geoblocked === null) { + geoblocked = await isUserGeoblocked(); + writeLocalStorageCache("geoblocking", geoblocked); + } + + return geoblocked; + }, staleTime: SEVEN_DAYS_MS, placeholderData: (prev) => prev, }); diff --git a/src/typescript/frontend/src/utils/index.ts b/src/typescript/frontend/src/utils/index.ts index 4eb2eb686..869d3c68c 100644 --- a/src/typescript/frontend/src/utils/index.ts +++ b/src/typescript/frontend/src/utils/index.ts @@ -12,11 +12,12 @@ export { getStylesFromResponsiveValue } from "./styled-components-helpers"; export { isDisallowedEventKey } from "./check-is-disallowed-event-key"; export { getEmptyListTr } from "./get-empty-list-tr"; -export const stringifyJSON = (data: object) => - JSON.stringify(data, (_, value) => { +export function stringifyJSON(data: T) { + return JSON.stringify(data, (_, value) => { if (typeof value === "bigint") return value.toString() + "n"; return value; }); +} export const parseJSON = (json: string): T => JSON.parse(json, (_, value) => { From aa481d60fe4e990c5474dfc0fe9b2c91aba3fd03 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Tue, 26 Nov 2024 05:42:34 +0100 Subject: [PATCH 76/94] [ECO-2275] Focus emoji picker on input click (#351) --- .../src/components/emoji-picker/EmojiPickerWithInput.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx b/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx index 498e1803a..026c9a352 100644 --- a/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx +++ b/src/typescript/frontend/src/components/emoji-picker/EmojiPickerWithInput.tsx @@ -239,7 +239,15 @@ export const EmojiPickerWithInput = ({ e.stopPropagation(); }} onClick={() => { + const shadowRoot = document.querySelector("em-emoji-picker") + ?.shadowRoot as ShadowRoot; + const pickerInputElement = shadowRoot.querySelector( + "div.search input" + ) as HTMLInputElement; setPickerInvisible(false); + if (pickerInvisible) { + pickerInputElement.focus(); + } }} data-testid="emoji-input" style={{ fontFamily: EMOJI_FONT_FAMILY }} From c67301c6742221a363a5b508f958b4fe9a7f8e8f Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Tue, 26 Nov 2024 18:29:10 +0100 Subject: [PATCH 77/94] [ECO-2502] Use swap as default tab on mobile (#413) --- .../components/pages/emojicoin/components/mobile-grid/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/mobile-grid/index.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/mobile-grid/index.tsx index 372ad3513..7d2cf7c56 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/mobile-grid/index.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/mobile-grid/index.tsx @@ -22,7 +22,7 @@ const DISPLAY_HEADER_ABOVE_CHART = false; const HEIGHT = DISPLAY_HEADER_ABOVE_CHART ? "min-h-[320px]" : "min-h-[365px]"; const MobileGrid = (props: GridProps) => { - const [tab, setTab] = useState(1); + const [tab, setTab] = useState(2); const { t } = translationFunction(); return ( From dc876bc403d7b0a641d35bf2c0f18a8ff49fdfe6 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:36:07 -0800 Subject: [PATCH 78/94] [ECO-2492] Calculate the swap price client-side (#412) --- .../trade-emojicoin/SwapComponent.tsx | 43 ++- .../lib/hooks/queries/use-simulate-swap.ts | 41 +++ .../src/lib/hooks/use-calculate-swap-price.ts | 72 +++++ src/typescript/package.json | 7 +- src/typescript/sdk/package.json | 8 +- .../sdk/src/client/emojicoin-client.ts | 49 ++- src/typescript/sdk/src/const.ts | 22 ++ .../emojicoin_dot_fun/calculate-swap-price.ts | 217 +++++++++++++ .../sdk/src/emojicoin_dot_fun/index.ts | 1 + src/typescript/sdk/src/markets/utils.ts | 7 +- .../sdk/tests/e2e/calculate-swap.test.ts | 296 ++++++++++++++++++ .../tests/e2e/exact-transition-amount.test.ts | 112 +++++++ src/typescript/sdk/tests/e2e/helpers/misc.ts | 44 +++ src/typescript/sdk/tests/pre-test.ts | 21 +- src/typescript/turbo.json | 6 +- 15 files changed, 898 insertions(+), 48 deletions(-) create mode 100644 src/typescript/frontend/src/lib/hooks/use-calculate-swap-price.ts create mode 100644 src/typescript/sdk/src/emojicoin_dot_fun/calculate-swap-price.ts create mode 100644 src/typescript/sdk/tests/e2e/calculate-swap.test.ts create mode 100644 src/typescript/sdk/tests/e2e/exact-transition-amount.test.ts diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx index 5f41fae9f..029003304 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx @@ -8,7 +8,7 @@ import { SwapButton } from "./SwapButton"; import { type SwapComponentProps } from "components/pages/emojicoin/types"; import { toActualCoinDecimals, toDisplayCoinDecimals } from "lib/utils/decimals"; import { useScramble } from "use-scramble"; -import { DEFAULT_SWAP_GAS_COST, useSimulateSwap } from "lib/hooks/queries/use-simulate-swap"; +import { DEFAULT_SWAP_GAS_COST, useGetGasWithDefault } from "lib/hooks/queries/use-simulate-swap"; import { useEventStore } from "context/event-store-context"; import { useTooltip } from "@hooks/index"; import { useSearchParams } from "next/navigation"; @@ -25,6 +25,7 @@ import { TradeOptions } from "components/selects/trade-options"; import { getMaxSlippageSettings } from "utils/slippage"; import { Emoji } from "utils/emoji"; import { EmojiPill } from "components/EmojiPill"; +import { useCalculateSwapPrice } from "lib/hooks/use-calculate-swap-price"; const SimulateInputsWrapper = ({ children }: PropsWithChildren) => (
{children}
@@ -89,29 +90,21 @@ export default function SwapComponent({ (s) => s.getMarket(marketEmojis)?.swapEvents.length ?? initNumSwaps ); - useEffect(() => { - const emojicoinType = toCoinTypes(marketAddress).emojicoin.toString(); - setEmojicoinType(emojicoinType); - }, [marketAddress, setEmojicoinType]); + const lastSwapEvent = useEventStore((s) => s.getMarket(marketEmojis)?.swapEvents?.at(0)); + + const gasCost = useGetGasWithDefault({ marketAddress, inputAmount, isSell, numSwaps }); - const swapData = useSimulateSwap({ - marketAddress, - inputAmount: inputAmount.toString(), + const { netProceeds, error } = useCalculateSwapPrice({ + lastSwapEvent, isSell, - numSwaps, + inputAmount, + userEmojicoinBalance: emojicoinBalance, }); - const { swapResult, gasCost, gasCostWasUndefined } = swapData - ? { - swapResult: swapData.swapResult, - gasCost: swapData.gasCost, - gasCostWasUndefined: false, - } - : { - swapResult: undefined, - gasCost: DEFAULT_SWAP_GAS_COST, - gasCostWasUndefined: true, - }; + useEffect(() => { + const emojicoinType = toCoinTypes(marketAddress).emojicoin.toString(); + setEmojicoinType(emojicoinType); + }, [marketAddress, setEmojicoinType]); const outputAmountString = toDisplayCoinDecimals({ num: isLoading ? previous : outputAmount, @@ -138,15 +131,15 @@ export default function SwapComponent({ }, [isLoading, replay]); useEffect(() => { - if (typeof swapResult === "undefined") { + if (netProceeds === 0n || error) { setIsLoading(true); return; } - setPrevious(swapResult); - setOutputAmount(swapResult); + setPrevious(netProceeds); + setOutputAmount(netProceeds); setIsLoading(false); replay(); - }, [swapResult, replay, isSell]); + }, [netProceeds, replay, isSell, error]); const sufficientBalance = useMemo(() => { if (!account || (isSell && !emojicoinBalance) || (!isSell && !aptBalance)) return false; @@ -299,7 +292,7 @@ export default function SwapComponent({
- {gasCostWasUndefined ? "~" : ""} + {gasCost === DEFAULT_SWAP_GAS_COST ? "~" : ""} {toDisplayCoinDecimals({ num: gasCost, decimals: 4, diff --git a/src/typescript/frontend/src/lib/hooks/queries/use-simulate-swap.ts b/src/typescript/frontend/src/lib/hooks/queries/use-simulate-swap.ts index 55fc97e45..479bd711f 100644 --- a/src/typescript/frontend/src/lib/hooks/queries/use-simulate-swap.ts +++ b/src/typescript/frontend/src/lib/hooks/queries/use-simulate-swap.ts @@ -92,6 +92,9 @@ export const simulateSwap = async (args: { * Simulate a swap with the view function. * The only three params that the user can change are the marketAddress, inputAmount, and isSell. * `numSwaps` is for invalidating the cache and refetching the query when the # of swaps changes. + * + * @deprecated in favor of calculating the swap price client-side instead of from the fullnode. + * @see {@link useCalculateSwapPrice} */ export const useSimulateSwap = (args: { marketAddress: AccountAddressString; @@ -165,3 +168,41 @@ export const useSimulateSwap = (args: { swapResult: isSell ? BigInt(data.quote_volume) : BigInt(data.base_volume), }; }; + +export const useGetGasWithDefault = (args: { + marketAddress: AccountAddressString; + inputAmount: bigint | number | string; + isSell: boolean; + numSwaps: number; +}) => { + const { marketAddress } = args; + const { emojicoin, emojicoinLP } = toCoinTypes(marketAddress); + const { aptos, account } = useAptos(); + const typeTags = [emojicoin, emojicoinLP] as [TypeTag, TypeTag]; + const { inputAmount, swapper, minOutputAmount } = useMemo(() => { + const bigInput = Big(args.inputAmount.toString()); + const inputAmount = BigInt(bigInput.toString()); + return { + invalid: inputAmount === 0n, + inputAmount, + minOutputAmount: 1n, + swapper: account?.address ? (account.address as `0x${string}`) : undefined, + }; + }, [args.inputAmount, account?.address]); + + const gas = useGetGas({ + aptos, + account, + ...args, + swapper, + inputAmount, + minOutputAmount, + typeTags, + }); + + // Neither of these values will ever be zero if a meaningful value is returned, + // so we can just check if it's truthy. + return gas && gas.gas_used && gas.gas_unit_price + ? BigInt(gas.gas_used) * BigInt(gas.gas_unit_price) + : DEFAULT_SWAP_GAS_COST; +}; diff --git a/src/typescript/frontend/src/lib/hooks/use-calculate-swap-price.ts b/src/typescript/frontend/src/lib/hooks/use-calculate-swap-price.ts new file mode 100644 index 000000000..720572816 --- /dev/null +++ b/src/typescript/frontend/src/lib/hooks/use-calculate-swap-price.ts @@ -0,0 +1,72 @@ +import { INITIAL_REAL_RESERVES, INITIAL_VIRTUAL_RESERVES } from "@sdk/const"; +import { + calculateSwapNetProceeds, + type SwapNetProceedsArgs, + SwapNotEnoughBaseError, +} from "@sdk/emojicoin_dot_fun/calculate-swap-price"; +import { type DatabaseModels } from "@sdk/indexer-v2/types"; +import { type AnyNumberString } from "@sdk/types/types"; + +/** + * This hook calls the client-side calculation of the swap net proceeds amount. + * If the Move contract logic would result in an error being thrown, it's captured + * in the `error` field returned by the calculation function called in this hook. + * + * In order to not disrupt the execution flow and see the resulting output swap price + * regardless of the input user balance, we re-run the client-side simulation if the input + * is invalid due to insufficient balance, but if the simulation results in a divide by + * zero error, we don't return, since that is due to invalid input amount. + */ +export const useCalculateSwapPrice = ({ + lastSwapEvent, + isSell, + inputAmount, + userEmojicoinBalance, +}: { + lastSwapEvent?: DatabaseModels["swap_events"]; + isSell: boolean; + inputAmount: AnyNumberString; + userEmojicoinBalance: AnyNumberString; +}) => { + const args: SwapNetProceedsArgs = { + ...getReservesAndBondingCurveStateWithDefault(lastSwapEvent), + isSell, + inputAmount, + userEmojicoinBalance, + }; + const res = calculateSwapNetProceeds(args); + // If the error is due to an insufficient user balance, + // simulate the calculation again, ensuring that the + // user emojicoin balance is sufficient. + if (res.error instanceof SwapNotEnoughBaseError) { + const { netProceeds: recalculatedNetProceeds } = calculateSwapNetProceeds({ + ...args, + userEmojicoinBalance: BigInt(args.inputAmount) + 1n, + }); + // Force the return type to show that the error was an insufficient balance. + return { + netProceeds: recalculatedNetProceeds, + error: res.error, + }; + } + // Otherwise, just return the result. + // Note that this \may return a divide by zero error still. + return res; +}; + +const getReservesAndBondingCurveStateWithDefault = ( + lastSwapEvent?: DatabaseModels["swap_events"] +) => { + if (lastSwapEvent) { + return { + clammVirtualReserves: lastSwapEvent.state.clammVirtualReserves, + cpammRealReserves: lastSwapEvent.state.cpammRealReserves, + startsInBondingCurve: lastSwapEvent.swap.startsInBondingCurve, + }; + } + return { + clammVirtualReserves: INITIAL_VIRTUAL_RESERVES, + cpammRealReserves: INITIAL_REAL_RESERVES, + startsInBondingCurve: false, + }; +}; diff --git a/src/typescript/package.json b/src/typescript/package.json index 25d3a5191..e7a879103 100644 --- a/src/typescript/package.json +++ b/src/typescript/package.json @@ -44,9 +44,10 @@ "test:frontend": " pnpm run load-env:test -- turbo run test --filter @econia-labs/emojicoin-frontend --log-prefix none", "test:frontend:e2e": "pnpm run load-env:test -- turbo run test:e2e --filter @econia-labs/emojicoin-frontend --log-prefix none", "test:sdk": "pnpm run load-env:test -- turbo run test --filter @econia-labs/emojicoin-sdk --log-prefix none", - "test:sdk:e2e": "pnpm run load-env:unit-test -- turbo run test:e2e --filter @econia-labs/emojicoin-sdk --force --log-prefix none --log-order grouped", - "test:sdk:unit": "NO_TEST_SETUP=true pnpm run load-env:test -- turbo run test:unit --force --log-prefix none --log-order grouped", - "test:verbose": "FETCH_DEBUG=true VERBOSE_TEST_LOGS=true pnpm run load-env:test -- turbo run test --force" + "test:sdk:e2e": "pnpm run load-env:test -- turbo run test:e2e --filter @econia-labs/emojicoin-sdk --force --log-prefix none", + "test:sdk:parallel": "pnpm run load-env:test -- turbo run test:sdk:parallel --filter @econia-labs/emojicoin-sdk --force --log-prefix none", + "test:sdk:sequential": "pnpm run load-env:test -- turbo run test:sdk:sequential --filter @econia-labs/emojicoin-sdk --force --log-prefix none", + "test:sdk:unit": "NO_TEST_SETUP=true pnpm run load-env:test -- turbo run test:unit --filter @econia-labs/emojicoin-sdk --force --log-prefix none" }, "version": "0.0.0", "workspaces": [ diff --git a/src/typescript/sdk/package.json b/src/typescript/sdk/package.json index e0fe663b6..2e1526999 100644 --- a/src/typescript/sdk/package.json +++ b/src/typescript/sdk/package.json @@ -67,10 +67,10 @@ "pre-commit": "pnpm run pre-commit:install && pnpm run pre-commit:run", "pre-commit:install": "pre-commit install -c ../../../cfg/pre-commit-config.yaml", "pre-commit:run": "pre-commit run --all-files -c ../../../cfg/pre-commit-config.yaml", - "test": "pnpm run test:parallel && pnpm run test:sequential && pnpm run test:unit", - "test:e2e": "pnpm run test:parallel && pnpm run test:sequential", - "test:parallel": "pnpm jest --testPathIgnorePatterns=tests/e2e/broker", - "test:sequential": "pnpm jest --runInBand tests/e2e/broker", + "test": "pnpm run test:sdk:parallel && pnpm run test:sdk:sequential && pnpm run test:unit", + "test:e2e": "pnpm run test:sdk:parallel && pnpm run test:sdk:sequential", + "test:sdk:parallel": "pnpm jest --testPathIgnorePatterns=tests/e2e/broker", + "test:sdk:sequential": "pnpm jest --runInBand tests/e2e/broker", "test:unit": "pnpm jest tests/unit" }, "typings": "dist/src/index.d.ts", diff --git a/src/typescript/sdk/src/client/emojicoin-client.ts b/src/typescript/sdk/src/client/emojicoin-client.ts index a38ab620a..98fbc7090 100644 --- a/src/typescript/sdk/src/client/emojicoin-client.ts +++ b/src/typescript/sdk/src/client/emojicoin-client.ts @@ -29,7 +29,7 @@ import { DEFAULT_REGISTER_MARKET_GAS_OPTIONS, INTEGRATOR_ADDRESS } from "../cons import { waitFor } from "../utils"; import { postgrest } from "../indexer-v2/queries"; import { TableName } from "../indexer-v2/types/json-types"; -import { type AnyNumberString } from "../types"; +import { toSwapEvent, type AnyNumberString } from "../types"; const { expect, Expect } = customExpect; @@ -124,8 +124,12 @@ export class EmojicoinClient { public view: { marketExists: typeof EmojicoinClient.prototype.isMarketRegisteredView; + simulateBuy: typeof EmojicoinClient.prototype.simulateBuy; + simulateSell: typeof EmojicoinClient.prototype.simulateSell; } = { marketExists: this.isMarketRegisteredView.bind(this), + simulateBuy: this.simulateBuy.bind(this), + simulateSell: this.simulateSell.bind(this), }; private integrator: AccountAddress; @@ -249,6 +253,49 @@ export class EmojicoinClient { ); } + private async simulateBuy(args: { + symbolEmojis: SymbolEmoji[]; + swapper: AccountAddressInput; + inputAmount: AnyNumberString; + ledgerVersion?: number | bigint; + }) { + return await this.simulateSwap({ ...args, isSell: false }); + } + + private async simulateSell(args: { + symbolEmojis: SymbolEmoji[]; + swapper: AccountAddressInput; + inputAmount: AnyNumberString; + ledgerVersion?: number | bigint; + }) { + return await this.simulateSwap({ ...args, isSell: true }); + } + + private async simulateSwap(args: { + symbolEmojis: SymbolEmoji[]; + swapper: AccountAddressInput; + inputAmount: AnyNumberString; + isSell: boolean; + ledgerVersion?: number | bigint; + }) { + const { symbolEmojis, swapper, inputAmount, isSell, ledgerVersion } = args; + const { marketAddress, typeTags } = this.getEmojicoinInfo(symbolEmojis); + const res = await EmojicoinDotFun.SimulateSwap.view({ + aptos: this.aptos, + swapper, + marketAddress, + inputAmount: BigInt(inputAmount), + isSell, + integrator: this.integrator, + integratorFeeRateBPs: this.integratorFeeRateBPs, + typeTags, + options: { + ledgerVersion, + }, + }); + return toSwapEvent(res, -1); + } + private async isMarketRegisteredView( symbolEmojis: SymbolEmoji[], ledgerVersion?: AnyNumberString diff --git a/src/typescript/sdk/src/const.ts b/src/typescript/sdk/src/const.ts index fcd7f267e..b7551a826 100644 --- a/src/typescript/sdk/src/const.ts +++ b/src/typescript/sdk/src/const.ts @@ -8,6 +8,7 @@ import { import Big from "big.js"; import { type ValueOf } from "./utils/utility-types"; import { type DatabaseStructType } from "./indexer-v2/types/json-types"; +import { type Types } from "./types"; export const VERCEL = process.env.VERCEL === "1"; if ( @@ -98,6 +99,27 @@ export const MARKET_REGISTRATION_FEE = ONE_APT_BIGINT; export const MARKET_REGISTRATION_DEPOSIT = 1n * ONE_APT_BIGINT; export const MARKET_REGISTRATION_GAS_ESTIMATION_NOT_FIRST = ONE_APT * 0.005; export const MARKET_REGISTRATION_GAS_ESTIMATION_FIRST = ONE_APT * 0.6; +export const BASIS_POINTS_PER_UNIT = 10_000n; +/** + * A market's virtual reserves upon creation. Used to calculate the swap price + * of a market when no swaps exist yet. + * + * @see {@link https://github.com/econia-labs/emojicoin-dot-fun/blob/295cf611950f66651452baa3e6ad6d6aef583f9b/src/move/emojicoin_dot_fun/sources/emojicoin_dot_fun.move#L2030} + */ +export const INITIAL_VIRTUAL_RESERVES: Types["Reserves"] = { + base: BASE_VIRTUAL_CEILING, + quote: QUOTE_VIRTUAL_FLOOR, +}; + +/** + * A market's real reserves upon creation. + * + * @see {@link INITIAL_VIRTUAL_RESERVES} + */ +export const INITIAL_REAL_RESERVES: Types["Reserves"] = { + base: 0n, + quote: 0n, +}; /// As defined in the database, aka the enum string. export enum Period { diff --git a/src/typescript/sdk/src/emojicoin_dot_fun/calculate-swap-price.ts b/src/typescript/sdk/src/emojicoin_dot_fun/calculate-swap-price.ts new file mode 100644 index 000000000..1388c1780 --- /dev/null +++ b/src/typescript/sdk/src/emojicoin_dot_fun/calculate-swap-price.ts @@ -0,0 +1,217 @@ +import { + BASE_VIRTUAL_FLOOR, + BASIS_POINTS_PER_UNIT, + EMOJICOIN_REMAINDER, + INTEGRATOR_FEE_RATE_BPS, + POOL_FEE_RATE_BPS, + QUOTE_REAL_CEILING, + QUOTE_VIRTUAL_CEILING, +} from "../const"; +import { type AnyNumberString, type Types } from "../types"; +import Big from "big.js"; + +export class CustomCalculatedSwapError extends Error { + constructor(msg: string) { + super(msg); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} + +export class SwapNotEnoughBaseError extends CustomCalculatedSwapError { + constructor() { + super("Not enough base to swap."); + } +} + +export class DivideByZeroError extends CustomCalculatedSwapError { + constructor() { + super("Can't divide by zero."); + } +} + +export type SwapNetProceedsArgs = { + clammVirtualReserves: Types["Reserves"]; + cpammRealReserves: Types["Reserves"]; + startsInBondingCurve: boolean; + isSell: boolean; + inputAmount: AnyNumberString; + userEmojicoinBalance: AnyNumberString; +}; + +export const getBPsFee = (principal: Big, feeRateBPs: AnyNumberString) => + principal + .mul(feeRateBPs.toString()) + .div(BASIS_POINTS_PER_UNIT.toString()) + .round(0, Big.roundDown); + +const cpammSimpleSwapOutputAmount = ({ + inputAmount, + isSell, + reserves, +}: { + inputAmount: Big; + isSell: boolean; + reserves: Types["Reserves"]; +}) => { + const [numeratorCoefficient, denominatorAddend] = isSell + ? [reserves.quote, reserves.base] + : [reserves.base, reserves.quote]; + const numerator = inputAmount.mul(numeratorCoefficient.toString()); + const denominator = inputAmount.plus(denominatorAddend.toString()); + if (!denominator.gt(0)) { + throw new DivideByZeroError(); + } + return numerator.div(denominator); +}; + +/** + * @throws @see {@link SwapNotEnoughBaseError} if the user does not have enough base + * @throws @see {@link DivideByZeroError} if the cpamm output calculation results in a divide by zero + * @see {@link https://github.com/econia-labs/emojicoin-dot-fun/blob/295cf611950f66651452baa3e6ad6d6aef583f9b/src/move/emojicoin_dot_fun/sources/emojicoin_dot_fun.move#L1691} + */ +const calculateExactSwapNetProceeds = (args: SwapNetProceedsArgs) => { + const { + clammVirtualReserves, + cpammRealReserves, + startsInBondingCurve, + userEmojicoinBalance, + isSell, + } = args; + const inputAmount = Big(BigInt(args.inputAmount).toString()); + const balanceBefore = Big(BigInt(userEmojicoinBalance).toString()); + + let poolFee: Big = Big(0); + let baseVolume: Big; + let quoteVolume: Big; + let integratorFee: Big; + let resultsInStateTransition = false; + + // -------------------------- Selling -------------------------- + if (isSell) { + let ammQuoteOutput: Big; + if (startsInBondingCurve) { + ammQuoteOutput = cpammSimpleSwapOutputAmount({ + inputAmount, + isSell, + reserves: clammVirtualReserves, + }); + } else { + ammQuoteOutput = cpammSimpleSwapOutputAmount({ + inputAmount, + isSell, + reserves: cpammRealReserves, + }); + poolFee = getBPsFee(ammQuoteOutput, POOL_FEE_RATE_BPS); + } + integratorFee = getBPsFee(ammQuoteOutput, INTEGRATOR_FEE_RATE_BPS); + baseVolume = inputAmount; /* eslint-disable-line */ + quoteVolume = ammQuoteOutput.minus(poolFee).minus(integratorFee); + const netProceeds = quoteVolume; + if (!inputAmount.lte(balanceBefore)) { + throw new SwapNotEnoughBaseError(); + } + // Unused, but kept here to preserve the Move code flow. + const _balanceAfter = balanceBefore.minus(inputAmount); + return netProceeds; + } else { + // -------------------------- Buying -------------------------- + integratorFee = getBPsFee(inputAmount, INTEGRATOR_FEE_RATE_BPS); + quoteVolume = inputAmount.minus(integratorFee); + if (startsInBondingCurve) { + const maxQuoteVolumeInClamm = Big( + BigInt(QUOTE_VIRTUAL_CEILING - clammVirtualReserves.quote).toString() + ); + if (quoteVolume.lt(maxQuoteVolumeInClamm)) { + baseVolume = cpammSimpleSwapOutputAmount({ + inputAmount: quoteVolume, + isSell, + reserves: clammVirtualReserves, + }); + } else { + // Max quote has been deposited to bonding curve. + resultsInStateTransition = true; + const _ = resultsInStateTransition; + // Clear out remaining base. + baseVolume = Big((clammVirtualReserves.base - BASE_VIRTUAL_FLOOR).toString()); + const remainingQuoteVolume = quoteVolume.minus(maxQuoteVolumeInClamm); + // Keep buying against CPAMM. + if (remainingQuoteVolume.gt(0)) { + // Evaluate swap against CPAMM with newly locked liquidity. + const cpammBaseOutput = cpammSimpleSwapOutputAmount({ + inputAmount: remainingQuoteVolume, + isSell, + reserves: { + base: EMOJICOIN_REMAINDER, + quote: QUOTE_REAL_CEILING, + }, + }); + poolFee = getBPsFee(cpammBaseOutput, POOL_FEE_RATE_BPS); + baseVolume = baseVolume.plus(cpammBaseOutput).minus(poolFee); + } + } + } else { + // Buying from CPAMM only. + const cpammBaseOutput = cpammSimpleSwapOutputAmount({ + inputAmount: quoteVolume, + isSell, + reserves: cpammRealReserves, + }); + const poolFee = getBPsFee(cpammBaseOutput, POOL_FEE_RATE_BPS); + baseVolume = cpammBaseOutput.minus(poolFee); + } + const netProceeds = baseVolume; + // Unused, but kept here to preserve the Move code flow. + const _balanceAfter = balanceBefore.plus(netProceeds); + return netProceeds; + } +}; + +type NetProceedsReturnTypes = + | { + netProceeds: bigint; + error: null; + } + | { + netProceeds: 0n; + error: CustomCalculatedSwapError; + }; + +/** + * The wrapper function for calculating the swap proceeds. This function rounds + * the returned value down like the Move contract does, since technically + * this code is more precise than the Move code with truncated uint values. + * + * @returns the total net proceeds- denominated in quote or volume based on `isSell`. + * @returns 0 if the calculation results in an error. + */ +export const calculateSwapNetProceeds = (args: { + clammVirtualReserves: Types["Reserves"]; + cpammRealReserves: Types["Reserves"]; + startsInBondingCurve: boolean; + isSell: boolean; + inputAmount: AnyNumberString; + userEmojicoinBalance: AnyNumberString; +}): NetProceedsReturnTypes => { + try { + const res = calculateExactSwapNetProceeds(args); + // Round down to zero decimal places like an unsigned integer will in Move. + const netProceeds = BigInt(res.round(0, Big.roundDown).toString()); + return { + netProceeds, + error: null, + }; + } catch (e) { + if (e instanceof CustomCalculatedSwapError) { + return { + netProceeds: 0n, + error: e, + }; + } + console.warn(`Unexpected error when calculating swap ${e}`); + return { + netProceeds: 0n, + error: null, + }; + } +}; diff --git a/src/typescript/sdk/src/emojicoin_dot_fun/index.ts b/src/typescript/sdk/src/emojicoin_dot_fun/index.ts index 4525fb057..96ffaa7e7 100644 --- a/src/typescript/sdk/src/emojicoin_dot_fun/index.ts +++ b/src/typescript/sdk/src/emojicoin_dot_fun/index.ts @@ -2,3 +2,4 @@ export * as AptosFramework from "./aptos-framework"; export * as EmojicoinDotFun from "./emojicoin-dot-fun"; export * from "./types"; export * from "./utils"; +export * from "./calculate-swap-price"; diff --git a/src/typescript/sdk/src/markets/utils.ts b/src/typescript/sdk/src/markets/utils.ts index 4ab4f1f7f..e1d0d8399 100644 --- a/src/typescript/sdk/src/markets/utils.ts +++ b/src/typescript/sdk/src/markets/utils.ts @@ -2,6 +2,7 @@ import { type Account, AccountAddress, type AccountAddressInput, + type AnyNumber, type Aptos, type AptosConfig, Hex, @@ -235,12 +236,16 @@ export const registerMarketAndGetEmojicoinInfo = async (args: { export async function getMarketResource(args: { aptos: Aptos; marketAddress: AccountAddressInput; + ledgerVersion?: AnyNumber; }): Promise { - const { aptos } = args; + const { aptos, ledgerVersion } = args; const marketAddress = AccountAddress.from(args.marketAddress); const marketResource = await aptos.getAccountResource({ accountAddress: marketAddress, resourceType: STRUCT_STRINGS.Market, + options: { + ledgerVersion, + }, }); return toMarketResource(marketResource); diff --git a/src/typescript/sdk/tests/e2e/calculate-swap.test.ts b/src/typescript/sdk/tests/e2e/calculate-swap.test.ts new file mode 100644 index 000000000..547fa5c51 --- /dev/null +++ b/src/typescript/sdk/tests/e2e/calculate-swap.test.ts @@ -0,0 +1,296 @@ +import { type AccountAddressInput } from "@aptos-labs/ts-sdk"; +import { + INTEGRATOR_FEE_RATE_BPS, + ONE_APT, + INITIAL_REAL_RESERVES, + INITIAL_VIRTUAL_RESERVES, +} from "../../src/const"; +import { + type AnyNumberString, + getMarketResource, + maxBigInt, + SYMBOL_EMOJI_DATA, + type SymbolEmoji, + toCoinTypes, + zip, +} from "../../src"; +import { + calculateSwapNetProceeds, + deriveEmojicoinPublisherAddress, +} from "../../src/emojicoin_dot_fun"; +import { getPublishHelpers } from "../../src/utils/test"; +import { getFundedAccounts } from "../../src/utils/test/test-accounts"; +import { EmojicoinClient } from "../../src/client/emojicoin-client"; +import { TransferCoins } from "../../src/emojicoin_dot_fun/aptos-framework"; +import { getExactTransitionInputAmount } from "./helpers/misc"; +import { getCoinBalanceFromChanges } from "../../src/utils/parse-changes-for-balances"; + +jest.setTimeout(30000); + +const exactTransitionInputAmount = getExactTransitionInputAmount(); + +describe("tests the swap functionality", () => { + const { aptos } = getPublishHelpers(); + const registrants = getFundedAccounts("058", "059", "060", "061", "062", "063"); + const secondaryTraders = getFundedAccounts("064", "065", "066", "067", "068"); + const marketEmojis = ([["👱"], ["👱🏻"], ["👱🏼"], ["👱🏽"], ["👱🏾"], ["👱🏿"]] as SymbolEmoji[][]).map( + (symbol) => symbol.map((e) => SYMBOL_EMOJI_DATA.byEmojiStrict(e)) + ); + const marketSymbols = marketEmojis.map((emojis) => emojis.map((e) => e.emoji)); + const emojicoin = new EmojicoinClient({ integratorFeeRateBPs: INTEGRATOR_FEE_RATE_BPS }); + let maxRegisterTxnVersion: bigint; + + // A helper function to get a market resource. We embed the latest market registration + // transaction version into this function so that all Market resources can be found on-chain at + // the time of the query. + let getMarketResourceHelper: ( + marketAddress: AccountAddressInput, + version: AnyNumberString + ) => ReturnType; + const getMarketAddress = (emojis: SymbolEmoji[]) => deriveEmojicoinPublisherAddress({ emojis }); + + beforeAll(async () => { + const versions = await Promise.all( + zip(registrants, marketEmojis).map(([registrant, marketEmojis]) => + emojicoin + .register( + registrant, + marketEmojis.map((emojiData) => emojiData.emoji) + ) + .then((res) => { + expect(res.response.success).toBe(true); + return BigInt(res.response.version); + }) + ) + ); + maxRegisterTxnVersion = maxBigInt(...versions); + getMarketResourceHelper = (marketAddress, version) => + getMarketResource({ aptos, marketAddress, ledgerVersion: BigInt(version) }); + return true; + }); + + it("first buyer on a market buys halfway through bonding curve", async () => { + const idx = 0; + const [firstSwapper, symbolEmojis] = [registrants[idx], marketSymbols[idx]]; + const isSell = false; + const inputAmount = ONE_APT * 500; + const marketAddress = getMarketAddress(marketSymbols[idx]); + const market = await getMarketResourceHelper(marketAddress, maxRegisterTxnVersion); + const { clammVirtualReserves, cpammRealReserves } = market; + + const viewSimulationOutput = await emojicoin.view.simulateBuy({ + symbolEmojis, + swapper: firstSwapper.accountAddress, + inputAmount, + }); + const { netProceeds } = calculateSwapNetProceeds({ + clammVirtualReserves, + cpammRealReserves, + startsInBondingCurve: true, + isSell, + inputAmount, + userEmojicoinBalance: inputAmount, + }); + expect(viewSimulationOutput.netProceeds).toEqual(netProceeds); + }); + + it("the second buyer on a market buys past the bonding curve", async () => { + const idx = 1; + const [firstSwapper, secondSwapper, symbolEmojis] = [ + registrants[idx], + secondaryTraders[idx], + marketSymbols[idx], + ]; + // Have the registrant buy just barely not enough to move through the bonding curve. + // That is, one more octa would mean it moves out of bonding curve from the swap buy. + const justNotEnough = exactTransitionInputAmount - 1n; + const res = await emojicoin.buy(firstSwapper, symbolEmojis, justNotEnough); + + const { model } = res.swap; + const marketAddress = getMarketAddress(symbolEmojis); + const market = await getMarketResourceHelper(marketAddress, res.response.version); + const { clammVirtualReserves, cpammRealReserves } = market; + + // Ensure the market HAS NOT progressed past the bonding curve by checking the resource fields. + expect(market.lpCoinSupply).toEqual(0n); + expect(model.state.lpCoinSupply).toEqual(0n); + expect(model.swap.startsInBondingCurve).toBe(true); + expect(model.swap.resultsInStateTransition).toBe(false); + expect(model.market.marketNonce).toEqual(market.sequenceInfo.nonce); + + const inputAmount = 123456n; + const viewSimulationOutput = await emojicoin.view.simulateBuy({ + symbolEmojis: symbolEmojis, + swapper: secondSwapper.accountAddress, + inputAmount, + }); + const { netProceeds } = calculateSwapNetProceeds({ + clammVirtualReserves, + cpammRealReserves, + startsInBondingCurve: true, + isSell: false, + inputAmount, + userEmojicoinBalance: inputAmount, + }); + expect(viewSimulationOutput.netProceeds).toEqual(netProceeds); + }); + + it("the second buyer on a market buys to an EXACT state transition", async () => { + const idx = 2; + const [firstSwapper, secondSwapper, symbolEmojis] = [ + registrants[idx], + secondaryTraders[idx], + marketSymbols[idx], + ]; + // The registrant buys `1n` worth of emojicoin and the second buyer buys the rest to finish + // the bonding curve. + const firstInputAmount = 1n; + const res = await emojicoin.buy(firstSwapper, symbolEmojis, firstInputAmount); + + const { model } = res.swap; + const marketAddress = getMarketAddress(symbolEmojis); + const market = await getMarketResourceHelper(marketAddress, res.response.version); + const { clammVirtualReserves, cpammRealReserves } = market; + + // Ensure the market HAS NOT progressed past the bonding curve by checking the resource fields. + expect(market.lpCoinSupply).toEqual(0n); + expect(model.state.lpCoinSupply).toEqual(0n); + expect(model.swap.startsInBondingCurve).toBe(true); + expect(model.swap.resultsInStateTransition).toBe(false); + expect(model.market.marketNonce).toEqual(market.sequenceInfo.nonce); + + const inputAmount = exactTransitionInputAmount - firstInputAmount; + const viewSimulationOutput = await emojicoin.view.simulateBuy({ + symbolEmojis: symbolEmojis, + swapper: secondSwapper.accountAddress, + inputAmount, + }); + const { netProceeds } = calculateSwapNetProceeds({ + clammVirtualReserves, + cpammRealReserves, + startsInBondingCurve: true, + isSell: false, + inputAmount, + userEmojicoinBalance: inputAmount, + }); + expect(viewSimulationOutput.netProceeds).toEqual(netProceeds); + }); + + it("the second trader on a market sells into the bonding curve", async () => { + const idx = 3; + const [firstSwapper, secondSwapper, symbolEmojis] = [ + registrants[idx], + secondaryTraders[idx], + marketSymbols[idx], + ]; + // Buy some random amount less than what's necessary to end the bonding curve. + const inputAmount = 74127356n; + const res = await emojicoin.buy(firstSwapper, symbolEmojis, inputAmount); + + const { model } = res.swap; + const marketAddress = getMarketAddress(symbolEmojis); + const market = await getMarketResourceHelper(marketAddress, res.response.version); + const { clammVirtualReserves, cpammRealReserves } = market; + + // Ensure the market HAS NOT progressed past the bonding curve. + expect(market.lpCoinSupply).toEqual(0n); + expect(model.state.lpCoinSupply).toEqual(0n); + expect(model.swap.startsInBondingCurve).toBe(true); + expect(model.swap.resultsInStateTransition).toBe(false); + expect(model.market.marketNonce).toEqual(market.sequenceInfo.nonce); + + // Transfer the emojicoins from the first swapper to the second. + const { emojicoin: emojicoinType } = toCoinTypes(marketAddress); + const transferRes = await TransferCoins.submit({ + aptosConfig: aptos.config, + from: firstSwapper, + to: secondSwapper.accountAddress, + amount: inputAmount, + typeTags: [emojicoinType], + }); + expect(transferRes.success).toBe(true); + + // Have the second trader sell into the bonding curve with the same input amount that the first + // trader (the registrant) bought earlier. + const viewSimulationOutput = await emojicoin.view.simulateSell({ + symbolEmojis: symbolEmojis, + swapper: secondSwapper.accountAddress, + inputAmount, + }); + const { netProceeds } = calculateSwapNetProceeds({ + clammVirtualReserves, + cpammRealReserves, + startsInBondingCurve: true, + isSell: true, + inputAmount, + userEmojicoinBalance: inputAmount, + }); + expect(viewSimulationOutput.netProceeds).toEqual(netProceeds); + }); + + it("the second trader on a market sells once it's outside the bonding curve", async () => { + const idx = 4; + const [firstSwapper, secondSwapper, symbolEmojis] = [ + registrants[idx], + secondaryTraders[idx], + marketSymbols[idx], + ]; + // Buy exactly enough to trigger a bonding curve transition. + const res = await emojicoin.buy(firstSwapper, symbolEmojis, exactTransitionInputAmount); + + const { model } = res.swap; + const marketAddress = getMarketAddress(symbolEmojis); + const market = await getMarketResourceHelper(marketAddress, res.response.version); + const { clammVirtualReserves, cpammRealReserves } = market; + + // Ensure the market HAS progressed past the bonding curve. + expect(market.lpCoinSupply).not.toEqual(0n); + expect(model.state.lpCoinSupply).not.toEqual(0n); + expect(model.swap.startsInBondingCurve).toBe(true); + expect(model.swap.resultsInStateTransition).toBe(true); + expect(model.market.marketNonce).toEqual(market.sequenceInfo.nonce); + + // Transfer the emojicoins from the first swapper to the second. + const { emojicoin: emojicoinType } = toCoinTypes(marketAddress); + const transferRes = await TransferCoins.submit({ + aptosConfig: aptos.config, + from: firstSwapper, + to: secondSwapper.accountAddress, + amount: res.swap.model.swap.netProceeds, + typeTags: [emojicoinType], + }); + expect(transferRes.success).toBe(true); + + const balance = getCoinBalanceFromChanges({ + response: transferRes, + userAddress: secondSwapper.accountAddress, + coinType: emojicoinType, + })!; + expect(balance).toBeDefined(); + expect(balance).toEqual(res.swap.model.swap.netProceeds); + + // Have the second trader sell the coins into a post-bonding curve market. + const viewSimulationOutput = await emojicoin.view.simulateSell({ + symbolEmojis: symbolEmojis, + swapper: secondSwapper.accountAddress, + inputAmount: balance, + }); + const { netProceeds } = calculateSwapNetProceeds({ + clammVirtualReserves, + cpammRealReserves, + startsInBondingCurve: false, + isSell: true, + inputAmount: balance, + userEmojicoinBalance: balance, + }); + expect(viewSimulationOutput.netProceeds).toEqual(netProceeds); + }); + + it("verifies that a market's initial virtual and real reserves are expected", async () => { + const idx = 5; + const marketAddress = getMarketAddress(marketSymbols[idx]); + const market = await getMarketResourceHelper(marketAddress, maxRegisterTxnVersion); + expect(market.clammVirtualReserves).toEqual(INITIAL_VIRTUAL_RESERVES); + expect(market.cpammRealReserves).toEqual(INITIAL_REAL_RESERVES); + }); +}); diff --git a/src/typescript/sdk/tests/e2e/exact-transition-amount.test.ts b/src/typescript/sdk/tests/e2e/exact-transition-amount.test.ts new file mode 100644 index 000000000..5abffed3c --- /dev/null +++ b/src/typescript/sdk/tests/e2e/exact-transition-amount.test.ts @@ -0,0 +1,112 @@ +import { INTEGRATOR_FEE_RATE_BPS } from "../../src/const"; +import { SYMBOL_EMOJI_DATA, type SymbolEmoji, zip } from "../../src"; +import { getFundedAccounts } from "../../src/utils/test/test-accounts"; +import { EmojicoinClient } from "../../src/client/emojicoin-client"; +import { getExactTransitionInputAmount } from "./helpers/misc"; +import { EXACT_TRANSITION_INPUT_AMOUNT } from "../../src/utils/test"; + +jest.setTimeout(30000); + +describe("tests the exact transition input amount with integrator fees calculations", () => { + const registrants = getFundedAccounts("069", "070", "071", "072"); + const marketEmojis = ([["☃️"], ["⛄"], ["🌨️"], ["❄️"]] as SymbolEmoji[][]).map((symbol) => + symbol.map((e) => SYMBOL_EMOJI_DATA.byEmojiStrict(e)) + ); + const marketSymbols = marketEmojis.map((emojis) => emojis.map((e) => e.emoji)); + const emojicoin = new EmojicoinClient({ integratorFeeRateBPs: INTEGRATOR_FEE_RATE_BPS }); + + beforeAll(async () => { + const responses = await Promise.all( + zip(registrants, marketEmojis).map(([registrant, marketEmojis]) => + emojicoin + .register( + registrant, + marketEmojis.map((emojiData) => emojiData.emoji) + ) + .then(({ response }) => response.success) + ) + ); + expect(responses.every((v) => v)); + return true; + }); + + it("exits the bonding curve with the default env integrator fee", async () => { + const idx = 0; + const [buyer, symbol] = [registrants[idx], marketSymbols[idx]]; + const inputAmount = getExactTransitionInputAmount(); + const res = await emojicoin.buy(buyer, symbol, inputAmount); + const { model } = res.swap; + + expect(model.swap.quoteVolume).toEqual(EXACT_TRANSITION_INPUT_AMOUNT); + expect(model.state.lpCoinSupply).not.toEqual(0n); + expect(model.swap.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); + expect(model.swap.startsInBondingCurve).toEqual(true); + expect(model.swap.resultsInStateTransition).toEqual(true); + }); + + it("just barely doesn't exit the bonding curve with the default env integrator fee", async () => { + const idx = 1; + const [buyer, symbol] = [registrants[idx], marketSymbols[idx]]; + const inputAmount = getExactTransitionInputAmount() - 1n; + const res = await emojicoin.buy(buyer, symbol, inputAmount); + const { model } = res.swap; + + expect(model.swap.quoteVolume).toEqual(EXACT_TRANSITION_INPUT_AMOUNT - 1n); + expect(model.state.lpCoinSupply).toEqual(0n); + expect(model.swap.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); + expect(model.swap.startsInBondingCurve).toEqual(true); + expect(model.swap.resultsInStateTransition).toEqual(false); + + // Push it over the bonding curve with the smallest amount possible. + const res2 = await emojicoin.buy(buyer, symbol, 1n); + const { model: model2 } = res2.swap; + expect(model2.state.lpCoinSupply).not.toEqual(0n); + expect(model2.swap.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); + expect(model2.swap.startsInBondingCurve).toEqual(true); + expect(model2.swap.resultsInStateTransition).toEqual(true); + }); + + it("exits the bonding curve with a custom integrator fee", async () => { + const idx = 2; + const customFee = 250; + const customIntegratorFeeClient = new EmojicoinClient({ integratorFeeRateBPs: customFee }); + const [buyer, symbol] = [registrants[idx], marketSymbols[idx]]; + const inputAmount = getExactTransitionInputAmount(customFee); + const res = await customIntegratorFeeClient.buy(buyer, symbol, inputAmount); + const { model } = res.swap; + + expect(model.swap.quoteVolume).toEqual(EXACT_TRANSITION_INPUT_AMOUNT); + expect(model.state.lpCoinSupply).not.toEqual(0n); + expect(model.swap.integratorFeeRateBPs).toEqual(customFee); + expect(model.swap.startsInBondingCurve).toEqual(true); + expect(model.swap.resultsInStateTransition).toEqual(true); + }); + + it("just barely doesn't exit the bonding curve with a custom integrator fee", async () => { + const idx = 3; + const customFee = 250; + const customIntegratorFeeClient = new EmojicoinClient({ integratorFeeRateBPs: customFee }); + const [buyer, symbol] = [registrants[idx], marketSymbols[idx]]; + const inputAmount = getExactTransitionInputAmount(customFee) - 1n; + const res = await customIntegratorFeeClient.buy(buyer, symbol, inputAmount); + const { model } = res.swap; + + expect(model.swap.quoteVolume).toEqual(EXACT_TRANSITION_INPUT_AMOUNT - 1n); + expect(model.state.lpCoinSupply).toEqual(0n); + expect(model.swap.integratorFeeRateBPs).toEqual(customFee); + expect(model.swap.startsInBondingCurve).toEqual(true); + expect(model.swap.resultsInStateTransition).toEqual(false); + + // Push it over the bonding curve with the smallest amount possible. + const res2 = await customIntegratorFeeClient.buy(buyer, symbol, 1n); + const { model: model2 } = res2.swap; + expect(model2.state.lpCoinSupply).not.toEqual(0n); + expect(model2.swap.integratorFeeRateBPs).toEqual(customFee); + expect(model2.swap.startsInBondingCurve).toEqual(true); + expect(model2.swap.resultsInStateTransition).toEqual(true); + }); + + it("calculates the correct no fee transition amount", () => { + expect(getExactTransitionInputAmount(0)).toEqual(EXACT_TRANSITION_INPUT_AMOUNT); + }); +}); diff --git a/src/typescript/sdk/tests/e2e/helpers/misc.ts b/src/typescript/sdk/tests/e2e/helpers/misc.ts index 7870c1b89..d0cdcfda4 100644 --- a/src/typescript/sdk/tests/e2e/helpers/misc.ts +++ b/src/typescript/sdk/tests/e2e/helpers/misc.ts @@ -1,13 +1,17 @@ import { type AccountAddressInput, type UserTransactionResponse } from "@aptos-labs/ts-sdk"; import { + BASIS_POINTS_PER_UNIT, getEvents, getMarketResourceFromWriteSet, + INTEGRATOR_FEE_RATE_BPS, Period, periodEnumToRawDuration, rawPeriodToEnum, type Types, } from "../../../src"; import postgres from "postgres"; +import Big from "big.js"; +import { EXACT_TRANSITION_INPUT_AMOUNT } from "../../../src/utils/test"; export const getTrackerFromWriteSet = ( res: UserTransactionResponse, @@ -46,3 +50,43 @@ export const getOneMinutePeriodicStateEvents = (res: UserTransactionResponse) => export const getDbConnection = () => { return postgres(process.env.DB_URL!); }; + +/** + * NOTE: This function *WILL NOT* work if the INTEGRATOR_FEE_RATE_BPS results in a fee output that + * can only be represented with repeating decimals. The rounding errors are cumbersome to account + * for and we will only use reasonably "nice" numbers like 100 or 250 for the integrator fee. + * + * Calculates the exact transition input amount including integrator fees. + * + * The calculation solves for the total input amount (i) given: + * - Known exact transition amount (E) without fees + * - Integrator fee percentage (FEE_PERCENTAGE) @see get_bps_fee in the Move contract. + * + * Mathematical derivation: + * 1. E = i - (i * FEE_PERCENTAGE) // Base equation + * 2. E = i * (1 - FEE_PERCENTAGE) // Factor out i + * 3. i = E / (1 - FEE_PERCENTAGE) // Solve for i + * + * Where: + * - E = EXACT_TRANSITION_INPUT_AMOUNT + * - i = total input amount including fees + * - FEE_PERCENTAGE = INTEGRATOR_FEE_RATE_BPS / BASIS_POINTS_PER_UNIT + * + * @returns {bigint} The whole number `bigint` ceiling of the exact input amount needed to exit the + * bonding curve, including integrator fees. + */ +export const getExactTransitionInputAmount = ( + integratorFeeRateBPs: number = INTEGRATOR_FEE_RATE_BPS +) => { + // prettier-ignore + const FEE_PERCENTAGE = Big(integratorFeeRateBPs) + .div(BASIS_POINTS_PER_UNIT.toString()); + + const exactAmount = Big(EXACT_TRANSITION_INPUT_AMOUNT.toString()).div( + Big(1).minus(FEE_PERCENTAGE) + ); + + const rounded = exactAmount.round(0, Big.roundDown); + + return BigInt(rounded.toString()); +}; diff --git a/src/typescript/sdk/tests/pre-test.ts b/src/typescript/sdk/tests/pre-test.ts index 3563d6171..52e14def5 100644 --- a/src/typescript/sdk/tests/pre-test.ts +++ b/src/typescript/sdk/tests/pre-test.ts @@ -1,7 +1,9 @@ /* eslint-disable no-underscore-dangle */ import WebSocket from "ws"; import { DockerTestHarness } from "../src/utils/test/docker/docker-test-harness"; -import { type ContainerName } from "../src/utils/test/docker/logs"; + +// process.env.NO_TEST_SETUP => to skip the docker container test setup, like for unit tests. +// process.env.FILTER_TEST_LOGS => quiet mode, don't output logs that print to the console a lot. export default async function preTest() { // @ts-expect-error Using `globalThis` as any for a polyfill for `WebSocket` in node.js. @@ -12,19 +14,16 @@ export default async function preTest() { if (setupTest && startDockerServices) { // Print an empty line to separate `Determining test suites to run...` from the logs. console.debug(); - const noLogs: Array = []; - // Only show more meaningful test logs by default. - if (!process.env.VERBOSE_TEST_LOGS) { - noLogs.push(...(["broker", "processor", "frontend", "postgres"] as Array)); - } - // @ts-expect-error Using `globalThis` as any. - globalThis.__DOCKER_LOGS_FILTER__ = noLogs; // -------------------------------------------------------------------------------------- // Start the docker containers. // -------------------------------------------------------------------------------------- // Start the Docker test harness without the frontend container. - await DockerTestHarness.run({ frontend: false }); - - // The docker container start-up script publishes the package on-chain. + await DockerTestHarness.run({ + frontend: false, + filterLogsFrom: + process.env.FILTER_TEST_LOGS === "true" + ? ["broker", "processor", "frontend", "postgres"] + : [], + }); } } diff --git a/src/typescript/turbo.json b/src/typescript/turbo.json index 72a8f32db..2b7b99883 100644 --- a/src/typescript/turbo.json +++ b/src/typescript/turbo.json @@ -73,15 +73,15 @@ "cache": false, "outputs": [] }, - "test:parallel": { + "test:sdk:parallel": { "cache": false, "outputs": [] }, - "test:sequential": { + "test:sdk:sequential": { "cache": false, "outputs": [] }, - "test:unit": { + "test:sdk:unit": { "cache": false, "outputs": [] } From 3d30860bfea32e56ebd88e4a668cec7d709b3283 Mon Sep 17 00:00:00 2001 From: Bogdan Crisan Date: Tue, 26 Nov 2024 19:42:09 +0100 Subject: [PATCH 79/94] [ECO-2496] Incorporate misc changes from the payment solution PRs (#410) Co-authored-by: alnoki <43892045+alnoki@users.noreply.github.com> --- .../frontend/src/app/home/HomePage.tsx | 30 +++++++++---------- .../src/components/header-spacer/index.tsx | 3 ++ .../src/components/header-spacer/module.css | 9 ++++++ .../frontend/src/components/header/index.tsx | 2 -- .../frontend/src/components/loading.tsx | 19 ------------ .../pages/emoji-picker/EmojiPicker.tsx | 22 +------------- .../pages/emojicoin/ClientEmojicoinPage.tsx | 2 +- .../emoji-table/components/FilterOptions.tsx | 6 +--- .../ClientLaunchEmojicoinPage.tsx | 2 +- .../src/components/pages/pools/styled.tsx | 1 - .../src/configs/local-storage-keys.ts | 6 ++-- .../StateStoreContextProviders.tsx | 5 ++-- .../frontend/src/context/providers.tsx | 26 ++++++++++++++++ .../src/lib/store/user-settings-store.ts | 26 ++++++++++++---- 14 files changed, 83 insertions(+), 76 deletions(-) create mode 100644 src/typescript/frontend/src/components/header-spacer/index.tsx create mode 100644 src/typescript/frontend/src/components/header-spacer/module.css diff --git a/src/typescript/frontend/src/app/home/HomePage.tsx b/src/typescript/frontend/src/app/home/HomePage.tsx index 85dc75754..ec7b07f18 100644 --- a/src/typescript/frontend/src/app/home/HomePage.tsx +++ b/src/typescript/frontend/src/app/home/HomePage.tsx @@ -28,24 +28,22 @@ export default async function HomePageComponent({ }: HomePageProps) { return ( <> -
-
- {priceFeed.length > 0 ? : } -
- -
- {children} - +
+ {priceFeed.length > 0 ? : } +
+
- - + {children} +
+ + ); } diff --git a/src/typescript/frontend/src/components/header-spacer/index.tsx b/src/typescript/frontend/src/components/header-spacer/index.tsx new file mode 100644 index 000000000..094391efa --- /dev/null +++ b/src/typescript/frontend/src/components/header-spacer/index.tsx @@ -0,0 +1,3 @@ +import "./module.css"; + +export const HeaderSpacer = () =>
; diff --git a/src/typescript/frontend/src/components/header-spacer/module.css b/src/typescript/frontend/src/components/header-spacer/module.css new file mode 100644 index 000000000..034e857bd --- /dev/null +++ b/src/typescript/frontend/src/components/header-spacer/module.css @@ -0,0 +1,9 @@ +.header-spacer { + padding-top: 93px; +} + +@media screen and (max-width: 526px) { + .header-spacer { + padding-top: 110px; + } +} diff --git a/src/typescript/frontend/src/components/header/index.tsx b/src/typescript/frontend/src/components/header/index.tsx index ac011e6a5..f8b3672af 100644 --- a/src/typescript/frontend/src/components/header/index.tsx +++ b/src/typescript/frontend/src/components/header/index.tsx @@ -19,7 +19,6 @@ import ButtonWithConnectWalletFallback from "./wallet-button/ConnectWalletButton import { useSearchParams } from "next/navigation"; import Link, { type LinkProps } from "next/link"; import { useEmojiPicker } from "context/emoji-picker-context"; -import { GeoblockedBanner } from "components/geoblocking"; const Header = ({ isOpen, setIsOpen }: HeaderProps) => { const { isDesktop } = useMatchBreakpoints(); @@ -110,7 +109,6 @@ const Header = ({ isOpen, setIsOpen }: HeaderProps) => { - ); }; diff --git a/src/typescript/frontend/src/components/loading.tsx b/src/typescript/frontend/src/components/loading.tsx index 0b59cf3c8..420131b89 100644 --- a/src/typescript/frontend/src/components/loading.tsx +++ b/src/typescript/frontend/src/components/loading.tsx @@ -7,19 +7,10 @@ import { getRandomSymbolEmoji, SYMBOL_EMOJI_DATA, type SymbolEmojiData } from "@ import { Emoji } from "utils/emoji"; import { usePathname } from "next/navigation"; import { EMOJI_PATH_INTRA_SEGMENT_DELIMITER, ONE_SPACE } from "utils/pathname-helpers"; -import { type EmojiMartData } from "./pages/emoji-picker/types"; -import { init } from "emoji-mart"; const unpathify = (pathEmojiName: string) => SYMBOL_EMOJI_DATA.byName(pathEmojiName.replaceAll(EMOJI_PATH_INTRA_SEGMENT_DELIMITER, ONE_SPACE)); -const data = fetch("https://cdn.jsdelivr.net/npm/@emoji-mart/data@latest/sets/15/native.json").then( - (res) => - res.json().then((data) => { - return data as EmojiMartData; - }) -); - export const Loading = ({ emojis, numEmojis, @@ -27,16 +18,6 @@ export const Loading = ({ emojis?: SymbolEmojiData[]; numEmojis?: number; }) => { - // Fetch/load the emoji picker data here to ensure that the picker has emoji data to use. - // Since the library only initializes when the picker component is rendered, the library won't have - // data on pages that don't use the picker component unless we explicitly call `init(...)` here. - useEffect(() => { - data.then((d) => { - init({ set: "native", data: d }); - }); - - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, []); const pathname = usePathname(); // Use the emojis in the path if we're on the `market` page. const emojisInPath = pathname diff --git a/src/typescript/frontend/src/components/pages/emoji-picker/EmojiPicker.tsx b/src/typescript/frontend/src/components/pages/emoji-picker/EmojiPicker.tsx index 17fa3133c..6c9fb8b50 100644 --- a/src/typescript/frontend/src/components/pages/emoji-picker/EmojiPicker.tsx +++ b/src/typescript/frontend/src/components/pages/emoji-picker/EmojiPicker.tsx @@ -4,21 +4,12 @@ import { type HTMLAttributes, Suspense, useEffect, type PointerEventHandler } from "react"; import { useEmojiPicker } from "context/emoji-picker-context"; import { default as Picker } from "@emoji-mart/react"; -import { init, SearchIndex } from "emoji-mart"; +import { SearchIndex } from "emoji-mart"; import { type EmojiMartData, type EmojiPickerSearchData, type EmojiSelectorData } from "./types"; import { unifiedCodepointsToEmoji } from "utils/unified-codepoint-to-emoji"; import { ECONIA_BLUE } from "theme/colors"; import RoundButton from "@icons/Minimize"; -// This is 400KB of lots of repeated data, we can use a smaller version of this if necessary later. -// TBH, we should probably just fork the library. -const data = fetch("https://cdn.jsdelivr.net/npm/@emoji-mart/data@latest/sets/15/native.json").then( - (res) => - res.json().then((data) => { - return data as EmojiMartData; - }) -); - export type SearchResult = Array; export const search = async (value: string): Promise => { @@ -41,17 +32,6 @@ export default function EmojiPicker( const host = document.querySelector("em-emoji-picker"); const insertEmojiTextInput = useEmojiPicker((s) => s.insertEmojiTextInput); - // Load the data from the emoji picker library and then extract the valid chat emojis from it. - // This is why it's not necessary to build/import a `chat-emojis.json` set, even though there is `symbol-emojis.json`. - // NOTE: We don't verify that the length of this set is equal to the number of valid chat emojis in the Move contract. - useEffect(() => { - data.then((d) => { - init({ set: "native", data: d }); - }); - - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, []); - useEffect(() => { setPickerRef(host as HTMLDivElement); }, [host, setPickerRef]); diff --git a/src/typescript/frontend/src/components/pages/emojicoin/ClientEmojicoinPage.tsx b/src/typescript/frontend/src/components/pages/emojicoin/ClientEmojicoinPage.tsx index 1d0a856b5..596040b32 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/ClientEmojicoinPage.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/ClientEmojicoinPage.tsx @@ -33,7 +33,7 @@ const ClientEmojicoinPage = (props: EmojicoinProps) => { useReliableSubscribe({ eventTypes: EVENT_TYPES }); return ( - + {isTablet || isMobile ? : } diff --git a/src/typescript/frontend/src/components/pages/home/components/emoji-table/components/FilterOptions.tsx b/src/typescript/frontend/src/components/pages/home/components/emoji-table/components/FilterOptions.tsx index 1e9a6170e..a3a7eef61 100644 --- a/src/typescript/frontend/src/components/pages/home/components/emoji-table/components/FilterOptions.tsx +++ b/src/typescript/frontend/src/components/pages/home/components/emoji-table/components/FilterOptions.tsx @@ -39,10 +39,6 @@ export const FilterOptionsComponent = ({ filter, onChange }: FilterOptionsCompon const animate = useUserSettings((s) => s.animate); const toggleAnimate = useUserSettings((s) => s.toggleAnimate); - const handler = () => { - toggleAnimate(); - }; - return ( - + ); diff --git a/src/typescript/frontend/src/components/pages/launch-emojicoin/ClientLaunchEmojicoinPage.tsx b/src/typescript/frontend/src/components/pages/launch-emojicoin/ClientLaunchEmojicoinPage.tsx index 8029ae9c6..0eb3c9fb6 100644 --- a/src/typescript/frontend/src/components/pages/launch-emojicoin/ClientLaunchEmojicoinPage.tsx +++ b/src/typescript/frontend/src/components/pages/launch-emojicoin/ClientLaunchEmojicoinPage.tsx @@ -137,7 +137,7 @@ const ClientLaunchEmojicoinPage = () => { }, [status]); return ( -
+
diff --git a/src/typescript/frontend/src/components/pages/pools/styled.tsx b/src/typescript/frontend/src/components/pages/pools/styled.tsx index a1703d41d..83a9adc18 100644 --- a/src/typescript/frontend/src/components/pages/pools/styled.tsx +++ b/src/typescript/frontend/src/components/pages/pools/styled.tsx @@ -6,7 +6,6 @@ export const StyledPoolsPage = styled.div` flex-direction: column; justify-content: center; align-items: center; - padding-top: 93px; flex-grow: 1; margin-top: 15px; diff --git a/src/typescript/frontend/src/configs/local-storage-keys.ts b/src/typescript/frontend/src/configs/local-storage-keys.ts index e36b0c3ff..804a95b45 100644 --- a/src/typescript/frontend/src/configs/local-storage-keys.ts +++ b/src/typescript/frontend/src/configs/local-storage-keys.ts @@ -5,12 +5,14 @@ const LOCAL_STORAGE_KEYS = { theme: `${packages.name}_theme`, language: `${packages.name}_language`, geoblocking: `${packages.name}_geoblocking`, + settings: `${packages.name}_settings`, }; const LOCAL_STORAGE_CACHE_TIME = { theme: Infinity, language: Infinity, geoblocking: 7 * 24 * 60 * 60 * 1000, // 7 days. + settings: Infinity, }; export type LocalStorageCache = { @@ -24,7 +26,7 @@ export type LocalStorageCache = { * with unexpected data types. */ export function readLocalStorageCache(key: keyof typeof LOCAL_STORAGE_KEYS): T | null { - const str = localStorage.getItem(key); + const str = localStorage.getItem(LOCAL_STORAGE_KEYS[key]); if (str === null) { return null; } @@ -44,5 +46,5 @@ export function writeLocalStorageCache(key: keyof typeof LOCAL_STORAGE_KEYS, expiry: new Date().getTime() + LOCAL_STORAGE_CACHE_TIME[key], data, }; - localStorage.setItem(key, stringifyJSON>(cache)); + localStorage.setItem(LOCAL_STORAGE_KEYS[key], stringifyJSON>(cache)); } diff --git a/src/typescript/frontend/src/context/event-store-context/StateStoreContextProviders.tsx b/src/typescript/frontend/src/context/event-store-context/StateStoreContextProviders.tsx index 321f4e3b6..ce1eaf26b 100644 --- a/src/typescript/frontend/src/context/event-store-context/StateStoreContextProviders.tsx +++ b/src/typescript/frontend/src/context/event-store-context/StateStoreContextProviders.tsx @@ -52,13 +52,12 @@ export const UserSettingsContext = createContext | n export interface UserSettingsProviderProps { children: ReactNode; - initialState?: UserSettingsStore; } -export const UserSettingsProvider = ({ children, initialState }: UserSettingsProviderProps) => { +export const UserSettingsProvider = ({ children }: UserSettingsProviderProps) => { const store = useRef>(); if (!store.current) { - store.current = createUserSettingsStore(initialState); + store.current = createUserSettingsStore(); } return ( {children} diff --git a/src/typescript/frontend/src/context/providers.tsx b/src/typescript/frontend/src/context/providers.tsx index c9f866934..66364a6b3 100644 --- a/src/typescript/frontend/src/context/providers.tsx +++ b/src/typescript/frontend/src/context/providers.tsx @@ -31,11 +31,24 @@ import { OKXWallet } from "@okwallet/aptos-wallet-adapter"; import { EmojiPickerProvider } from "./emoji-picker-context/EmojiPickerContextProvider"; import { isMobile, isTablet } from "react-device-detect"; import { getAptosApiKey } from "@sdk/const"; +import type { EmojiMartData } from "components/pages/emoji-picker/types"; +import { init } from "emoji-mart"; +import { HeaderSpacer } from "components/header-spacer"; +import { GeoblockedBanner } from "components/geoblocking"; enableMapSet(); const queryClient = new QueryClient(); +// This is 400KB of lots of repeated data, we can use a smaller version of this if necessary later. +// TBH, we should probably just fork the library. +const data = fetch("https://cdn.jsdelivr.net/npm/@emoji-mart/data@latest/sets/15/native.json").then( + (res) => + res.json().then((data) => { + return data as EmojiMartData; + }) +); + const ThemedApp: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { theme } = useThemeContext(); const [isOpen, setIsOpen] = useState(false); @@ -48,6 +61,17 @@ const ThemedApp: React.FC<{ children: React.ReactNode }> = ({ children }) => { [] ); + // Load the data from the emoji picker library and then extract the valid chat emojis from it. + // This is why it's not necessary to build/import a `chat-emojis.json` set, even though there is `symbol-emojis.json`. + // NOTE: We don't verify that the length of this set is equal to the number of valid chat emojis in the Move contract. + useEffect(() => { + data.then((d) => { + init({ set: "native", data: d }); + }); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, []); + return ( @@ -74,6 +98,8 @@ const ThemedApp: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ + {children}