From b5d17a317e124a1f04ba537b818e5ffcba563885 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:05:59 -0800 Subject: [PATCH] [ECO-2561] Production release `v1.0.0` (#475) Co-authored-by: Bogdan Crisan Co-authored-by: alnoki <43892045+alnoki@users.noreply.github.com> --- cfg/cspell-frontend-dictionary.txt | 1 + src/move/market_metadata/README.md | 27 +++ src/move/rewards/README.md | 21 ++- src/typescript/example.env | 3 + src/typescript/frontend/package.json | 2 +- .../src/app/dexscreener/asset/route.ts | 18 +- .../src/app/dexscreener/events/route.ts | 130 +++++++------ .../src/app/dexscreener/latest-block/route.ts | 8 +- .../src/app/dexscreener/pair/route.ts | 17 +- .../src/components/FormattedNumber.tsx | 5 +- .../frontend/src/components/button/index.tsx | 60 +++--- .../components/main-info/BondingProgress.tsx | 115 +++++++----- .../components/main-info/MainInfo.tsx | 176 ++++++++++++++---- .../pages/metadata/MetadataPage.tsx | 13 +- .../svg/icons/BondingCurveArrow.tsx | 19 ++ .../src/components/svg/icons/LogoIcon.tsx | 12 +- .../src/components/svg/icons/Planet.tsx | 15 +- .../src/components/svg/icons/Telegram.tsx | 19 ++ .../svg/icons/TelegramOutlineIcon.tsx | 19 ++ .../src/configs/local-storage-keys.ts | 31 ++- src/typescript/frontend/src/lib/env.ts | 3 + src/typescript/frontend/src/utils/emoji.tsx | 9 +- .../frontend/src/utils/geolocation.ts | 28 ++- .../contract-apis/market-metadata.ts | 2 +- src/typescript/sdk/src/markets/utils.ts | 39 +++- src/typescript/sdk/src/utils/utility-types.ts | 5 + 26 files changed, 579 insertions(+), 218 deletions(-) create mode 100644 src/typescript/frontend/src/components/svg/icons/BondingCurveArrow.tsx create mode 100644 src/typescript/frontend/src/components/svg/icons/Telegram.tsx create mode 100644 src/typescript/frontend/src/components/svg/icons/TelegramOutlineIcon.tsx diff --git a/cfg/cspell-frontend-dictionary.txt b/cfg/cspell-frontend-dictionary.txt index d105d041e..dcaf19d61 100644 --- a/cfg/cspell-frontend-dictionary.txt +++ b/cfg/cspell-frontend-dictionary.txt @@ -54,3 +54,4 @@ testid ADBEEF bytea nominalize +dexscreener diff --git a/src/move/market_metadata/README.md b/src/move/market_metadata/README.md index 08d8cc7b1..436305a19 100644 --- a/src/move/market_metadata/README.md +++ b/src/move/market_metadata/README.md @@ -30,6 +30,33 @@ aptos move publish \ --profile $PROFILE ``` +## Add admin + +```sh +NEW_ADMIN=0xccc... +MARKET_METADATA=0xaaa... +PROFILE_NAME=my-profile + +aptos move run \ + --args address:$NEW_ADMIN \ + --function-id $MARKET_METADATA::emojicoin_dot_fun_market_metadata::add_admin \ + --profile $PROFILE_NAME +``` + +## Remove admin + +```sh +ADMIN_TO_REMOVE=0xccc... +MARKET_METADATA=0xaaa... +PROFILE_NAME=my-profile + +aptos move run \ + --args address:$ADMIN_TO_REMOVE \ + --function-id \ + $MARKET_METADATA::emojicoin_dot_fun_market_metadata::remove_admin \ + --profile $PROFILE_NAME +``` + ## Set property ```sh diff --git a/src/move/rewards/README.md b/src/move/rewards/README.md index 788c741ef..f2f2c0958 100644 --- a/src/move/rewards/README.md +++ b/src/move/rewards/README.md @@ -1,18 +1,29 @@ # `emojicoin-dot-fun` rewards -This package contains an overloaded version of the `swap` function for -`emojicoin-dot-fun`, `swap_with_rewards`, which gives users an opportunity for a -reward based on the amount of integrator fees they pay for their swap. +## `emojicoin_dot_fun_rewards` -The rewards vault can be loaded up via the `fund_tiers` function. +This module contains an overloaded version of the `swap` function for +`emojicoin-dot-fun`, `emojicoin_dot_fun_rewards::swap_with_rewards`, which gives +users an opportunity for a reward based on the amount of integrator fees they +pay for their swap. Rewards distributions are random, and based on a probabilistic threshold determined by the number of nominal rewards distributions expected for a given -nominal volume amount. See `reward_tiers` for more. +nominal volume amount. See `emojicoin_dot_fun_rewards::reward_tiers` for more. For ease of parameter modeling, named constants use values in `APT`, which are converted to octas internally. +The rewards vault can be loaded up via the +`emojicoin_dot_fun_rewards::fund_tiers` function. + +## `emojicoin_dot_fun_claim_link` + +This module was designed to enable claim links, like for "magic link" sign on, +which would rely on private keys in QR codes or similar in order to claim +emojicoins. It was never used beyond the initial Move implementation, but is +kept here for reference. + ## Publish commands Set variables: diff --git a/src/typescript/example.env b/src/typescript/example.env index db42ac9e1..564acae9b 100644 --- a/src/typescript/example.env +++ b/src/typescript/example.env @@ -31,6 +31,9 @@ NEXT_PUBLIC_LINKS='{ "tos": "" }' +# Discord channel name for adding social links. +NEXT_PUBLIC_DISCORD_METADATA_REQUEST_CHANNEL="" + # The URL for the indexer's `postgrest` REST API. EMOJICOIN_INDEXER_URL="http://localhost:3000" diff --git a/src/typescript/frontend/package.json b/src/typescript/frontend/package.json index 3a6b10b84..3c4e682c8 100644 --- a/src/typescript/frontend/package.json +++ b/src/typescript/frontend/package.json @@ -108,5 +108,5 @@ "test:e2e": "playwright test --project=firefox", "vercel-install": "./submodule.sh && pnpm i" }, - "version": "0.0.1-alpha" + "version": "1.0.0" } diff --git a/src/typescript/frontend/src/app/dexscreener/asset/route.ts b/src/typescript/frontend/src/app/dexscreener/asset/route.ts index 16746b59f..03cba3e95 100644 --- a/src/typescript/frontend/src/app/dexscreener/asset/route.ts +++ b/src/typescript/frontend/src/app/dexscreener/asset/route.ts @@ -26,6 +26,7 @@ import { EMOJICOIN_SUPPLY } from "@sdk/const"; import { calculateCirculatingSupply } from "@sdk/markets"; import { symbolEmojiStringToArray } from "../util"; import { fetchMarketState } from "@/queries/market"; +import { toNominal } from "lib/utils/decimals"; /** * - In most cases, asset ids will correspond to contract addresses. Ids are case-sensitive. @@ -63,26 +64,35 @@ async function getAsset(assetId: string): Promise { const circulatingSupply: { circulatingSupply?: number | string } = {}; if (marketState && marketState.state) { - circulatingSupply.circulatingSupply = calculateCirculatingSupply(marketState.state).toString(); + circulatingSupply.circulatingSupply = toNominal(calculateCirculatingSupply(marketState.state)); } return { id: assetId, name: marketEmojiData.symbolData.name, symbol: marketEmojiData.symbolData.symbol, - totalSupply: Number(EMOJICOIN_SUPPLY), + totalSupply: toNominal(EMOJICOIN_SUPPLY), ...circulatingSupply, // coinGeckoId: assetId, // coinMarketCapId: assetId, }; } -// NextJS JSON response handler +// Although this route would be ideal for caching, nextjs doesn't offer the ability to control +// caches for failed responses. In other words, if someone queries an asset that doesn't exist +// yet at this endpoint, it would permanently cache that asset as not existing and thus return +// the failed query JSON response. This is obviously problematic for not yet existing markets, +// so unless we have some way to not cache failed queries/empty responses, we can't cache this +// endpoint at all. +export const revalidate = 0; +export const dynamic = "force-dynamic"; +export const fetchCache = "force-no-store"; + export async function GET(request: NextRequest): Promise> { const searchParams = request.nextUrl.searchParams; const assetId = searchParams.get("id"); if (!assetId) { - // This is a required field, and is an error otherwise + // This is a required field, and is an error otherwise. return new NextResponse("id is a parameter", { status: 400 }); } const asset = await getAsset(assetId); diff --git a/src/typescript/frontend/src/app/dexscreener/events/route.ts b/src/typescript/frontend/src/app/dexscreener/events/route.ts index 78159a246..1ab68e09d 100644 --- a/src/typescript/frontend/src/app/dexscreener/events/route.ts +++ b/src/typescript/frontend/src/app/dexscreener/events/route.ts @@ -88,7 +88,27 @@ import { calculateCurvePrice, calculateRealReserves } from "@sdk/markets"; import { toCoinDecimalString } from "../../../lib/utils/decimals"; import { DECIMALS } from "@sdk/const"; import { symbolEmojisToPairId } from "../util"; -import { compareBigInt } from "@econia-labs/emojicoin-sdk"; +import { compareBigInt, type Flatten } from "@econia-labs/emojicoin-sdk"; +import { type XOR } from "@sdk/utils/utility-types"; + +export type Asset0In1Out = { + asset0In: number | string; + asset1Out: number | string; +}; + +export type Asset1In0Out = { + asset0Out: number | string; + asset1In: number | string; +}; + +export type AssetInOut = XOR; + +export type DexscreenerReserves = { + reserves: { + asset0: number | string; + asset1: number | string; + }; +}; /** * - `txnId` is a transaction identifier such as a transaction hash @@ -128,24 +148,19 @@ import { compareBigInt } from "@econia-labs/emojicoin-sdk"; * - The Indexer automatically handles calculations for USD pricing (`priceUsd` as opposed to * `priceNative`) */ -export interface SwapEvent { - eventType: "swap"; - txnId: string; - txnIndex: number; - eventIndex: number; - maker: string; - pairId: string; - asset0In?: number | string; - asset1In?: number | string; - asset0Out?: number | string; - asset1Out?: number | string; - priceNative: number | string; - reserves?: { - asset0: number | string; - asset1: number | string; - }; - metadata?: Record; -} +export type SwapEvent = Flatten< + { + eventType: "swap"; + txnId: string; + txnIndex: number; + eventIndex: number; + maker: string; + pairId: string; + priceNative: number | string; + metadata?: Record; + } & AssetInOut & + DexscreenerReserves +>; /** * - `txnId` is a transaction identifier such as a transaction hash @@ -167,21 +182,19 @@ export interface SwapEvent { * - `metadata` includes any optional auxiliary info not covered in the default schema and not * required in most cases */ -interface JoinExitEvent { - eventType: "join" | "exit"; - txnId: string; - txnIndex: number; - eventIndex: number; - maker: string; - pairId: string; - amount0: number | string; - amount1: number | string; - reserves?: { - asset0: number | string; - asset1: number | string; - }; - metadata?: Record; -} +type JoinExitEvent = Flatten< + { + eventType: "join" | "exit"; + txnId: string; + txnIndex: number; + eventIndex: number; + maker: string; + pairId: string; + amount0: number | string; + amount1: number | string; + metadata?: Record; + } & DexscreenerReserves +>; type BlockInfo = { block: Block }; type Event = (SwapEvent | JoinExitEvent) & BlockInfo; @@ -189,27 +202,18 @@ type Event = (SwapEvent | JoinExitEvent) & BlockInfo; interface EventsResponse { events: Event[]; } - function toDexscreenerSwapEvent(event: ReturnType): SwapEvent & BlockInfo { - let assetInOut; - - if (event.swap.isSell) { - // We are selling to APT - assetInOut = { - asset0In: toCoinDecimalString(event.swap.inputAmount, DECIMALS), - asset0Out: 0, - asset1In: 0, - asset1Out: toCoinDecimalString(event.swap.baseVolume, DECIMALS), - }; - } else { - // We are buying with APT - assetInOut = { - asset0In: 0, - asset0Out: toCoinDecimalString(event.swap.quoteVolume, DECIMALS), - asset1In: toCoinDecimalString(event.swap.inputAmount, DECIMALS), - asset1Out: 0, - }; - } + // Base / quote is emojicoin / APT. + // Thus asset0 / asset1 is always base volume / quote volume. + const assetInOut = event.swap.isSell + ? { + asset0In: toCoinDecimalString(event.swap.baseVolume, DECIMALS), + asset1Out: toCoinDecimalString(event.swap.quoteVolume, DECIMALS), + } + : { + asset0Out: toCoinDecimalString(event.swap.baseVolume, DECIMALS), + asset1In: toCoinDecimalString(event.swap.quoteVolume, DECIMALS), + }; const { base, quote } = calculateRealReserves(event.state); const reserves = { @@ -224,7 +228,7 @@ function toDexscreenerSwapEvent(event: ReturnType): Swa return { block: { blockNumber: Number(event.blockAndEvent.blockNumber), - blockTimestamp: event.transaction.timestamp.getTime() / 1000, + blockTimestamp: Math.floor(event.transaction.timestamp.getTime() / 1000), }, eventType: "swap", txnId: event.transaction.version.toString(), @@ -236,11 +240,8 @@ function toDexscreenerSwapEvent(event: ReturnType): Swa pairId: symbolEmojisToPairId(event.market.symbolEmojis), ...assetInOut, - - asset0In: event.swap.inputAmount.toString(), - asset1Out: event.swap.quoteVolume.toString(), priceNative, - ...reserves, + reserves, }; } @@ -258,7 +259,7 @@ function toDexscreenerJoinExitEvent( return { block: { blockNumber: Number(event.blockAndEvent.blockNumber), - blockTimestamp: event.transaction.timestamp.getTime() / 1000, + blockTimestamp: Math.floor(event.transaction.timestamp.getTime() / 1000), }, eventType: event.liquidity.liquidityProvided ? "join" : "exit", @@ -290,7 +291,14 @@ async function getEventsByVersion(fromBlock: number, toBlock: number): Promise; } @@ -34,7 +34,8 @@ interface LatestBlockResponse { block: Block; } -// NextJS JSON response handler +export const revalidate = 1; + export async function GET(_request: NextRequest): Promise> { const status = await getProcessorStatus(); const aptos = getAptosClient(); @@ -47,8 +48,7 @@ export async function GET(_request: NextRequest): Promise> { const searchParams = request.nextUrl.searchParams; const pairId = searchParams.get("id"); diff --git a/src/typescript/frontend/src/components/FormattedNumber.tsx b/src/typescript/frontend/src/components/FormattedNumber.tsx index d7ff246aa..369bce203 100644 --- a/src/typescript/frontend/src/components/FormattedNumber.tsx +++ b/src/typescript/frontend/src/components/FormattedNumber.tsx @@ -75,7 +75,10 @@ export const FormattedNumber = ({ const num = nominalize ? toNominal(value as bigint) : Number(value); const format = style === "fixed" || Math.abs(num) >= 1 - ? { maximumFractionDigits: decimals } + ? { + maximumFractionDigits: decimals, + minimumFractionDigits: style === "fixed" ? decimals : undefined, + } : { maximumSignificantDigits: decimals }; const formatter = new Intl.NumberFormat("en-US", format); return formatter.format(num); diff --git a/src/typescript/frontend/src/components/button/index.tsx b/src/typescript/frontend/src/components/button/index.tsx index e36dfe26f..17fe1d4ed 100644 --- a/src/typescript/frontend/src/components/button/index.tsx +++ b/src/typescript/frontend/src/components/button/index.tsx @@ -17,13 +17,18 @@ const Button = ({ children, isLoading = false, disabled = false, + fakeDisabled = false, external, isScramble = true, scale = "sm", variant = "outline", scrambleProps = {}, + icon, ...rest -}: ButtonProps & { scrambleProps?: UseScrambleProps }): JSX.Element => { +}: ButtonProps & { scrambleProps?: UseScrambleProps } & { + icon?: React.ReactNode; + fakeDisabled?: boolean; +}): JSX.Element => { const isDisabled = isLoading || disabled; const internalProps = external ? EXTERNAL_LINK_PROPS : {}; @@ -34,6 +39,13 @@ const Button = ({ ...scrambleProps, }); + const textProps = { + textScale: "pixelHeading4" as const, + color: isDisabled || fakeDisabled ? ("darkGray" as const) : ("econiaBlue" as const), + textTransform: "uppercase" as const, + fontSize: scale === "sm" ? ("20px" as const) : ("24px" as const), + }; + return ( ({ })} {!isScramble ? ( - `{ ${children} }` - ) : ( - - - {"{ "} - - - - {" }"} + + {"{ "} + {icon && ( + + {icon} + + )} + + {children} + {" }"} + + ) : ( + + {"{ "} + {icon && ( + + {icon} + + )} + + {" }"} )} diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/main-info/BondingProgress.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/main-info/BondingProgress.tsx index c017e5254..7945e936c 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/main-info/BondingProgress.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/main-info/BondingProgress.tsx @@ -3,19 +3,16 @@ import React, { useEffect, useState } from "react"; import { translationFunction } from "context/language-context"; import { type MainInfoProps } from "../../types"; import { useEventStore } from "context/event-store-context"; -import { motion } from "framer-motion"; import { getBondingCurveProgress } from "@sdk/utils/bonding-curve"; -import Button from "components/button"; -import Link from "next/link"; -import { ROUTES } from "router/routes"; -import { useThemeContext } from "context"; import { FormattedNumber } from "components/FormattedNumber"; +import BondingCurveArrow from "@icons/BondingCurveArrow"; +import { emoji } from "utils"; +import { Emoji } from "utils/emoji"; -const statsTextClasses = "uppercase ellipses font-forma text-[24px]"; +const statsTextClasses = "uppercase ellipses font-forma"; const BondingProgress = ({ data }: MainInfoProps) => { const { t } = translationFunction(); - const { theme } = useThemeContext(); const marketEmojis = data.symbolEmojis; const stateEvents = useEventStore((s) => s.getMarket(marketEmojis)?.stateEvents ?? []); @@ -33,51 +30,85 @@ const BondingProgress = ({ data }: MainInfoProps) => { }, [stateEvents]); return ( -
-
-
+
+ {/* + 3.26 is calculated like this: + + The aspect ratio of a bonding curve arrow is 115/30 aka 23/6. + + There are 7 of them. + + So the aspect ratio of the container element is 115/30*7 aka 161/6. + + We know that the rocket emoji's width and height is 175% of the container height. + + We want to add padding to the container to include the half part of the rocket emoji that overflows on the left, in order to properly center the container within its container. + + The padding should be 50% of the rocket's width, but we cannot use the rocket width as a unit in CSS. + + But we know that the rocket width is 175% of the container height. + + But we cannot specify the left padding in height percentage, but only in width percentage. + + But we know that the container's width is 161/6 times the container's height. + + So we can do 100 / (161/6) * 1.75 / 2 which gives us ~3.26. + + Who knew CSS could be this hard... + */} +
+
+ + 15 ? "econiaBlue" : "darkGray"} + /> + 30 ? "econiaBlue" : "darkGray"} + /> + 45 ? "econiaBlue" : "darkGray"} + /> + 60 ? "econiaBlue" : "darkGray"} + /> + 75 ? "econiaBlue" : "darkGray"} + /> + 90 ? "econiaBlue" : "darkGray"} + /> + = 100 ? "econiaBlue" : "darkGray"} + /> +
+
+
+
{t("Bonding progress:")}
- {bondingProgress >= 100 ? ( - e.emoji).join("") }, - }} - > - - - ) : ( -
- -
- )}
); }; 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 2fd4d88b4..cf2e9d6a4 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 @@ -11,13 +11,66 @@ import { useMatchBreakpoints } from "@hooks/index"; import { Emoji } from "utils/emoji"; import Link from "next/link"; import { toExplorerLink } from "lib/utils/explorer-link"; -import { emoji } from "utils"; -import { motion } from "framer-motion"; -import { truncateAddress } from "@sdk/utils"; import { FormattedNumber } from "components/FormattedNumber"; +import Button from "components/button"; +import { Planet, TwitterOutlineIcon } from "components/svg"; +import TelegramOutlineIcon from "@icons/TelegramOutlineIcon"; +import { motion } from "framer-motion"; +import { MarketProperties } from "@/contract-apis"; +import { useAptos } from "context/wallet-context/AptosContextProvider"; +import type { Colors } from "theme/types"; +import Popup from "components/popup"; +import { DISCORD_METADATA_REQUEST_CHANNEL, LINKS } from "lib/env"; const statsTextClasses = "uppercase ellipses font-forma text-[24px]"; +const LinkButton = ({ + name, + link, + icon, +}: { + name: string; + link: string | undefined; + icon?: (color: keyof Colors) => React.ReactNode; +}) => { + const button = ( + + ); + if (link) { + return ( + + {button} + + ); + } + if (LINKS?.discord && DISCORD_METADATA_REQUEST_CHANNEL) { + return ( + + +
{button}
+ +
+ ); + } + return button; +}; + const MainInfo = ({ data }: MainInfoProps) => { const { t } = translationFunction(); const { theme } = useThemeContext(); @@ -43,14 +96,67 @@ const MainInfo = ({ data }: MainInfoProps) => { } }, [stateEvents]); - const { isMobile } = useMatchBreakpoints(); + const { isMobile, isTablet } = useMatchBreakpoints(); const explorerLink = toExplorerLink({ linkType: "coin", value: `${data.marketView.metadata.marketAddress}::coin_factory::Emojicoin`, }); - const [copied, setCopied] = useState(false); + const borderStyle = "border-solid border-[1px] border-dark-gray rounded-[3px] p-[1em]"; + + const [marketProperties, setMarketProperties] = useState>(); + + const { aptos } = useAptos(); + + useEffect(() => { + MarketProperties.view({ + aptos, + market: data.marketAddress, + }) + .then((r) => r.vec.at(0) ?? null) + .then((r) => { + if (r) { + const newFields = new Map(); + (r as { data: { key: string; value: string }[] }).data.forEach(({ key, value }) => { + newFields.set(key, value); + }); + setMarketProperties(newFields); + } + }) + .catch((e) => console.error("Could not get market metadata.", e)); + /* eslint-disable react-hooks/exhaustive-deps */ + }, [data.marketAddress]); + + const dexscreenerButton = ; + + const telegramButton = ( + ( + + )} + /> + ); + + const twitterButton = ( + ( + + )} + /> + ); + + const websiteButton = ( + } + /> + ); return (
{ }} >
-
+
-
- - {truncateAddress(data.marketView.metadata.marketAddress)} - - { - navigator.clipboard.writeText(data.marketView.metadata.marketAddress); - if (!copied) { - setCopied(true); - setTimeout(() => setCopied(false), 3000); - } - }} - > - - -
-
+
{t("Market Cap:")}
@@ -141,10 +225,26 @@ const MainInfo = ({ data }: MainInfoProps) => {
- -
- +
+
+
+ { + navigator.clipboard.writeText(data.marketView.metadata.marketAddress); + }} + whileTap={{ scaleX: 0.96, scaleY: 0.98 }} + transition={{ ease: "linear", duration: 0.05 }} + > + + + {dexscreenerButton} + {twitterButton} + {telegramButton} + {websiteButton}
+
diff --git a/src/typescript/frontend/src/components/pages/metadata/MetadataPage.tsx b/src/typescript/frontend/src/components/pages/metadata/MetadataPage.tsx index 773b60728..46491ee73 100644 --- a/src/typescript/frontend/src/components/pages/metadata/MetadataPage.tsx +++ b/src/typescript/frontend/src/components/pages/metadata/MetadataPage.tsx @@ -133,7 +133,7 @@ const MetadataPage = () => { if (pasted) { return; } - MarketProperties.submit({ + MarketProperties.view({ aptos, market: marketAddress, }) @@ -244,15 +244,16 @@ const MetadataPage = () => { if (!isSubmitEnabled) { return; } - const filledFields = fields.entries().filter(([_, value]) => value !== ""); - const builderLambda = () => - SetMarketProperties.builder({ + const filledFields = Array.from(fields.entries().filter(([_, value]) => value !== "")); + const builderLambda = () => { + return SetMarketProperties.builder({ aptosConfig: aptos.config, admin: account!.address, market: marketAddress, - keys: Array.from(filledFields.map(([key, _]) => key)), - values: Array.from(filledFields.map(([_, value]) => value)), + keys: filledFields.map(([key, _]) => key), + values: filledFields.map(([_, value]) => value), }); + }; const res = await submit(builderLambda); if (!res || res.error) { console.error(res); diff --git a/src/typescript/frontend/src/components/svg/icons/BondingCurveArrow.tsx b/src/typescript/frontend/src/components/svg/icons/BondingCurveArrow.tsx new file mode 100644 index 000000000..3dac57e93 --- /dev/null +++ b/src/typescript/frontend/src/components/svg/icons/BondingCurveArrow.tsx @@ -0,0 +1,19 @@ +"use client"; + +import React from "react"; +import Svg from "components/svg/Svg"; +import { type SvgProps } from "../types"; +import { darkColors } from "theme"; + +const Icon: React.FC = ({ color = "darkGray", ...props }) => { + return ( + + + + ); +}; + +export default Icon; diff --git a/src/typescript/frontend/src/components/svg/icons/LogoIcon.tsx b/src/typescript/frontend/src/components/svg/icons/LogoIcon.tsx index 6f73464d6..2a4d25d29 100644 --- a/src/typescript/frontend/src/components/svg/icons/LogoIcon.tsx +++ b/src/typescript/frontend/src/components/svg/icons/LogoIcon.tsx @@ -24,12 +24,14 @@ const Badge: React.FC> = ({ chi ); }; -const VersionBadge: React.FC<{ color: keyof Colors }> = ({ color }) => ( +const VersionBadge: React.FC<{ color: keyof Colors }> = ({ color }) => + // prettier-ignore - {VERSION?.prerelease[0].toString().toUpperCase()} v{VERSION?.major}.{VERSION?.minor}. - {VERSION?.patch} - -); + {VERSION?.prerelease.at(0)?.toString().toUpperCase()} + {VERSION?.prerelease.at(0) && <> } + v + {VERSION?.major}.{VERSION?.minor}.{VERSION?.patch} + ; const Icon: React.FC = ({ color = "econiaBlue", diff --git a/src/typescript/frontend/src/components/svg/icons/Planet.tsx b/src/typescript/frontend/src/components/svg/icons/Planet.tsx index b709359d2..3f942b43c 100644 --- a/src/typescript/frontend/src/components/svg/icons/Planet.tsx +++ b/src/typescript/frontend/src/components/svg/icons/Planet.tsx @@ -1,13 +1,18 @@ +"use client"; + import React from "react"; -import { type SVGProps } from "react"; +import type { SvgProps } from "../types"; +import Svg from "../Svg"; +import { useThemeContext } from "context"; -const Planet = (props: SVGProps) => { +const Planet: React.FC = ({ color = "econiaBlue", ...props }) => { + const { theme } = useThemeContext(); return ( - + @@ -15,7 +20,7 @@ const Planet = (props: SVGProps) => { - + ); }; diff --git a/src/typescript/frontend/src/components/svg/icons/Telegram.tsx b/src/typescript/frontend/src/components/svg/icons/Telegram.tsx new file mode 100644 index 000000000..f13678a8e --- /dev/null +++ b/src/typescript/frontend/src/components/svg/icons/Telegram.tsx @@ -0,0 +1,19 @@ +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 new file mode 100644 index 000000000..3aa99291b --- /dev/null +++ b/src/typescript/frontend/src/components/svg/icons/TelegramOutlineIcon.tsx @@ -0,0 +1,19 @@ +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/configs/local-storage-keys.ts b/src/typescript/frontend/src/configs/local-storage-keys.ts index c34084076..f3459cca3 100644 --- a/src/typescript/frontend/src/configs/local-storage-keys.ts +++ b/src/typescript/frontend/src/configs/local-storage-keys.ts @@ -1,6 +1,7 @@ import { parseJSON, stringifyJSON } from "utils"; import packages from "../../package.json"; import { MS_IN_ONE_DAY } from "components/charts/const"; +import { satisfies, type SemVer, parse } from "semver"; const LOCAL_STORAGE_KEYS = { theme: `${packages.name}_theme`, @@ -9,7 +10,18 @@ const LOCAL_STORAGE_KEYS = { settings: `${packages.name}_settings`, }; -export const LOCAL_STORAGE_CACHE_TIME = { +const LOCAL_STORAGE_VERSIONS: { + [Property in keyof typeof LOCAL_STORAGE_KEYS]: SemVer; +} = { + theme: parse("1.0.0")!, + language: parse("1.0.0")!, + geoblocking: parse("2.0.0")!, + settings: parse("1.0.0")!, +}; + +export const LOCAL_STORAGE_CACHE_TIME: { + [Property in keyof typeof LOCAL_STORAGE_KEYS]: number; +} = { theme: Infinity, language: Infinity, geoblocking: MS_IN_ONE_DAY, @@ -19,13 +31,9 @@ export const LOCAL_STORAGE_CACHE_TIME = { export type LocalStorageCache = { expiry: number; data: T | null; + version: string | undefined; }; -/** - * 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(LOCAL_STORAGE_KEYS[key]); if (str === null) { @@ -33,6 +41,16 @@ export function readLocalStorageCache(key: keyof typeof LOCAL_STORAGE_KEYS): } try { const cache = parseJSON>(str); + const range = `~${LOCAL_STORAGE_VERSIONS[key].major}`; + // Check for no breaking changes. + if (!satisfies(cache.version ?? "1.0.0", range)) { + console.warn( + `${key} cache version not satisfied (needs to satisfy ${range}, but ${cache.version} is present). Purging...` + ); + localStorage.delete(LOCAL_STORAGE_KEYS[key]); + return null; + } + // Check for staleness. if (new Date(cache.expiry) > new Date()) { return cache.data; } @@ -46,6 +64,7 @@ export function writeLocalStorageCache(key: keyof typeof LOCAL_STORAGE_KEYS, const cache: LocalStorageCache = { expiry: new Date().getTime() + LOCAL_STORAGE_CACHE_TIME[key], data, + version: LOCAL_STORAGE_VERSIONS[key].version, }; localStorage.setItem(LOCAL_STORAGE_KEYS[key], stringifyJSON>(cache)); } diff --git a/src/typescript/frontend/src/lib/env.ts b/src/typescript/frontend/src/lib/env.ts index 8cc4ea1b3..217a2faf4 100644 --- a/src/typescript/frontend/src/lib/env.ts +++ b/src/typescript/frontend/src/lib/env.ts @@ -22,6 +22,8 @@ let INTEGRATOR_ADDRESS: AccountAddressString; let INTEGRATOR_FEE_RATE_BPS: number; let BROKER_URL: string; let CDN_URL: string; +const DISCORD_METADATA_REQUEST_CHANNEL: string | undefined = + process.env.NEXT_PUBLIC_DISCORD_METADATA_REQUEST_CHANNEL; export const LINKS: Links | undefined = typeof process.env.NEXT_PUBLIC_LINKS === "string" && process.env.NEXT_PUBLIC_LINKS !== "" @@ -59,6 +61,7 @@ const VERSION = parse(packageInfo.version); export { APTOS_NETWORK, CDN_URL, + DISCORD_METADATA_REQUEST_CHANNEL, INTEGRATOR_ADDRESS, INTEGRATOR_FEE_RATE_BPS, IS_ALLOWLIST_ENABLED, diff --git a/src/typescript/frontend/src/utils/emoji.tsx b/src/typescript/frontend/src/utils/emoji.tsx index 29b0ae280..64a91d8b3 100644 --- a/src/typescript/frontend/src/utils/emoji.tsx +++ b/src/typescript/frontend/src/utils/emoji.tsx @@ -10,6 +10,7 @@ declare global { size?: string; native?: string; key?: string; + set?: string; }; } } @@ -17,19 +18,23 @@ declare global { export const Emoji = ({ emojis, + set = undefined, + size = "1em", ...props }: Omit, HTMLSpanElement>, "children"> & { emojis: AnyEmojiData[] | string; + set?: string; + size?: 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/geolocation.ts b/src/typescript/frontend/src/utils/geolocation.ts index 35b814630..3b96e5f0e 100644 --- a/src/typescript/frontend/src/utils/geolocation.ts +++ b/src/typescript/frontend/src/utils/geolocation.ts @@ -4,17 +4,31 @@ import { GEOBLOCKED, GEOBLOCKING_ENABLED } from "lib/server-env"; import { headers } from "next/headers"; export type Location = { - countryCode: string; - regionCode: string; + countryCode: string | null; + regionCode: string | null; }; const isDisallowedLocation = ({ countryCode, regionCode }: Location) => { - if (GEOBLOCKED.countries.includes(countryCode)) { + if (countryCode && GEOBLOCKED.countries.includes(countryCode)) { return true; } - const isoCode = `${countryCode}-${regionCode}`; - if (GEOBLOCKED.regions.includes(isoCode)) { - return true; + if (regionCode) { + const isoCode = `${countryCode}-${regionCode}`; + if (GEOBLOCKED.regions.includes(isoCode)) { + return true; + } + } + if (countryCode && !regionCode) { + if (GEOBLOCKED.regions.map((r) => r.split("-")[0]).includes(countryCode)) { + return true; + } + } + if (!countryCode && regionCode) { + // Note that even if the `regionCode` is `XX`, and `XX` is a banned country, this will return + // `true` and thus block the user, because "XX".split("-")[0] is just "XX". + if (GEOBLOCKED.countries.includes(regionCode.split("-")[0])) { + return true; + } } return false; }; @@ -23,7 +37,7 @@ export const isUserGeoblocked = async () => { if (!GEOBLOCKING_ENABLED) return false; const country = headers().get("x-vercel-ip-country"); const region = headers().get("x-vercel-ip-country-region"); - if (typeof country !== "string" || typeof region !== "string") { + if (typeof country !== "string" && typeof region !== "string") { return true; } return isDisallowedLocation({ diff --git a/src/typescript/sdk/src/emojicoin_dot_fun/contract-apis/market-metadata.ts b/src/typescript/sdk/src/emojicoin_dot_fun/contract-apis/market-metadata.ts index 3b5a31631..a199c72aa 100644 --- a/src/typescript/sdk/src/emojicoin_dot_fun/contract-apis/market-metadata.ts +++ b/src/typescript/sdk/src/emojicoin_dot_fun/contract-apis/market-metadata.ts @@ -565,7 +565,7 @@ export class MarketProperties extends ViewFunctionPayloadBuilder<[Option + isInBondingCurve(args) ? clammVirtualReserves : cpammRealReserves; + /** * *NOTE*: If you already have a market's state, call {@link calculateRealReserves} directly. * @@ -456,7 +466,34 @@ PreciseBig.DP = 100; * This is equivalent to calculating the slope of the tangent line created from the exact point on * the curve, where the curve is the function the AMM uses to calculate the price for the market. * - * The price is denominated in `quote / base`, where `base` is the emojicoin and `quote` is APT. + * The price is denominated in `base / quote`, but this is *NOT* a mathematical representation. + * Since the `base` is simply `1`, in a `base / quote` representation, the order of base and quote + * don't actually mean anything other than which one is being expressed as the whole number `1`. + * + * That is, `base / quote` does not imply that you divide `base` by `quote` to get the price, it + * just indicates how the price is going to be expressed semantically. What it really means is + * that you are describing the price in terms of `1 base` per `P quote`. + * + * To calculate this with the reserves, we need to find `P` below: + * + * 1 emojicoin / P APT + * + * We already know the reserves, now just structure them as the same fraction: + * + * emojicoin_reserves / APT_reserves + * + * Thus: + * + * 1 / P = emojicoin_reserves / APT_reserves + * => + * 1 = (emojicoin_reserves / APT_reserves) * P + * => + * 1 / (emojicoin_reserves / APT_reserves) = P + * => + * APT_reserves / emojicoin_reserves = P + * => + * Thus, when APT = quote and emojicoin = base, + * the price P = (quote.reserves / base.reserves) * * * For an in depth explanation of the math and behavior behind the AMMs: * @see {@link https://github.com/econia-labs/emojicoin-dot-fun/blob/main/doc/blackpaper/emojicoin-dot-fun-blackpaper.pdf} diff --git a/src/typescript/sdk/src/utils/utility-types.ts b/src/typescript/sdk/src/utils/utility-types.ts index b23ea9b35..86b65efec 100644 --- a/src/typescript/sdk/src/utils/utility-types.ts +++ b/src/typescript/sdk/src/utils/utility-types.ts @@ -36,3 +36,8 @@ export type Writable = { export type DeepWritable = { -readonly [P in keyof T]: DeepWritable; }; + +// prettier-ignore +export type XOR = + | (T & { [K in keyof U]?: never }) + | (U & { [K in keyof T]?: never });