diff --git a/src/rust/processor b/src/rust/processor index 84e72a0d5..de5d38e2a 160000 --- a/src/rust/processor +++ b/src/rust/processor @@ -1 +1 @@ -Subproject commit 84e72a0d5a9fac85d5102bea63e63cee4d056496 +Subproject commit de5d38e2a2581cc623c60410d389038167dadc50 diff --git a/src/typescript/frontend/src/app/home/HomePage.tsx b/src/typescript/frontend/src/app/home/HomePage.tsx index ec7b07f18..67411b24f 100644 --- a/src/typescript/frontend/src/app/home/HomePage.tsx +++ b/src/typescript/frontend/src/app/home/HomePage.tsx @@ -6,18 +6,16 @@ import TextCarousel from "components/text-carousel/TextCarousel"; import { type MarketDataSortByHomePage } from "lib/queries/sorting/types"; export interface HomePageProps { - featured?: DatabaseModels["market_state"]; markets: Array; numMarkets: number; page: number; sortBy: MarketDataSortByHomePage; searchBytes?: string; children?: React.ReactNode; - priceFeed: Array; + priceFeed: DatabaseModels["price_feed"][]; } export default async function HomePageComponent({ - featured, markets, numMarkets, page, @@ -30,8 +28,8 @@ export default async function HomePageComponent({ <>
{priceFeed.length > 0 ? : } -
- +
+
{children} diff --git a/src/typescript/frontend/src/app/home/page.tsx b/src/typescript/frontend/src/app/home/page.tsx index 02a45349a..303fef860 100644 --- a/src/typescript/frontend/src/app/home/page.tsx +++ b/src/typescript/frontend/src/app/home/page.tsx @@ -1,17 +1,19 @@ import { type HomePageParams, toHomePageParamsWithDefault } from "lib/routes/home-page-params"; import HomePageComponent from "./HomePage"; import { - fetchFeaturedMarket, fetchMarkets, fetchMarketsWithCount, fetchNumRegisteredMarkets, - fetchPriceFeed, + fetchPriceFeedWithMarketState, } 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"; import { unstable_cache } from "next/cache"; +import { parseJSON, stringifyJSON } from "utils"; +import { type DatabaseModels, toPriceFeed } from "@sdk/indexer-v2/types"; +import { type DatabaseJsonType } from "@sdk/indexer-v2/types/json-types"; +import { SortMarketsBy } from "@sdk/indexer-v2/types/common"; +import { ORDER_BY } from "@sdk/queries"; export const revalidate = 2; @@ -21,11 +23,31 @@ const getCachedNumMarketsFromAptosNode = unstable_cache( { revalidate: 10 } ); +const NUM_MARKETS_ON_PRICE_FEED = 25; + +const stringifiedFetchPriceFeedData = () => + fetchPriceFeedWithMarketState({ + sortBy: SortMarketsBy.DailyVolume, + orderBy: ORDER_BY.DESC, + pageSize: NUM_MARKETS_ON_PRICE_FEED, + }).then((res) => stringifyJSON(res)); + +const getCachedPriceFeedData = unstable_cache( + stringifiedFetchPriceFeedData, + ["price-feed-with-market-data"], + { 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; - const priceFeedPromise = fetchPriceFeed({}); + const priceFeedPromise = getCachedPriceFeedData() + .then((res) => parseJSON(res).map((p) => toPriceFeed(p))) + .catch((err) => { + console.error(err); + return [] as DatabaseModels["price_feed"][]; + }); let marketsPromise: ReturnType; @@ -53,16 +75,7 @@ export default async function Home({ searchParams }: HomePageParams) { numMarketsPromise = getCachedNumMarketsFromAptosNode(); } - let featuredPromise: ReturnType; - - if (sortBy === SortMarketsBy.DailyVolume && orderBy === ORDER_BY.DESC) { - featuredPromise = marketsPromise.then((r) => r[0]); - } else { - featuredPromise = fetchFeaturedMarket(); - } - - const [featured, priceFeed, markets, numMarkets] = await Promise.all([ - featuredPromise, + const [priceFeedData, markets, numMarkets] = await Promise.all([ priceFeedPromise, marketsPromise, numMarketsPromise, @@ -70,13 +83,12 @@ export default async function Home({ searchParams }: HomePageParams) { return ( ); } 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 f595f77a2..de81020ce 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 @@ -1,8 +1,8 @@ "use client"; -import React, { useEffect, useMemo, useRef } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { translationFunction } from "context/language-context"; -import { Column, Flex, FlexGap } from "@containers"; +import { FlexGap } from "@containers"; import { toCoinDecimalString } from "lib/utils/decimals"; import AptosIconBlack from "components/svg/icons/AptosBlack"; import Image from "next/image"; @@ -12,26 +12,52 @@ import { useLabelScrambler } from "../table-card/animation-variants/event-varian 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 { emoji } from "utils"; import { Emoji } from "utils/emoji"; +import "./module.css"; +import { PriceDelta } from "components/price-feed/inner"; +import { sortByValue } from "lib/utils/sort-events"; +import { AnimatePresence, motion } from "framer-motion"; +import { useInterval } from "react-use"; export interface MainCardProps { - featured?: HomePageProps["featured"]; + featuredMarkets: HomePageProps["priceFeed"]; page: HomePageProps["page"]; sortBy: HomePageProps["sortBy"]; } +const FEATURED_MARKET_INTERVAL = 5 * 1000; +const MAX_NUM_FEATURED_MARKETS = 5; + const MainCard = (props: MainCardProps) => { - const { featured } = props; + const featuredMarkets = useMemo(() => { + const sorted = props.featuredMarkets.toSorted((a, b) => + sortByValue(a.deltaPercentage, b.deltaPercentage, "desc") + ); + const positives = sorted.filter(({ deltaPercentage }) => deltaPercentage > 0); + const notPositives = sorted.filter(({ deltaPercentage }) => deltaPercentage <= 0); + return positives.length + ? positives.slice(0, MAX_NUM_FEATURED_MARKETS) + : notPositives.slice(0, MAX_NUM_FEATURED_MARKETS); + }, [props.featuredMarkets]); + const { t } = translationFunction(); const globeImage = useRef(null); - const { marketCap, dailyVolume, allTimeVolume } = useMemo(() => { + const [currentIndex, setCurrentIndex] = useState(0); + + useInterval(() => { + setCurrentIndex((i) => (i + 1) % Math.min(featuredMarkets.length, MAX_NUM_FEATURED_MARKETS)); + }, FEATURED_MARKET_INTERVAL); + + const featured = useMemo(() => featuredMarkets.at(currentIndex), [featuredMarkets, currentIndex]); + + const { marketCap, dailyVolume, allTimeVolume, priceDelta } = useMemo(() => { return { marketCap: BigInt(featured?.state.instantaneousStats.marketCap ?? 0), dailyVolume: BigInt(featured?.dailyVolume ?? 0), allTimeVolume: BigInt(featured?.state.cumulativeStats.quoteVolume ?? 0), + priceDelta: featured?.deltaPercentage ?? 0, }; }, [featured]); @@ -56,26 +82,15 @@ const MainCard = (props: MainCardProps) => { const { ref: allTimeVolumeRef } = useLabelScrambler(toCoinDecimalString(allTimeVolume, 2)); return ( - - +
+
x.name))}` : ROUTES.home } - style={{ - position: "relative", - alignItems: "center", - marginLeft: "-8%", - display: "flex", - }} > { src={planetHome} ref={globeImage} placeholder="empty" + className="z-10" /> - + +
+ + + +
+
- - +
- HOT + Hot  
+ {priceDelta > 0 && ( + + )}
- {(featured ? featured.market.symbolData.name : "BLACK HEART").toUpperCase()} + {featured?.market.symbolData.name ?? "BLACK HEART"}
- {typeof featured !== "undefined" && ( + {featured && ( <>
{t("Mkt. Cap:")} @@ -123,7 +161,7 @@ const MainCard = (props: MainCardProps) => { - {typeof featured !== "undefined" && ( + {featured && ( <>
@@ -142,7 +180,7 @@ const MainCard = (props: MainCardProps) => { - {typeof featured !== "undefined" && ( + {featured && ( <>
{t("All-time vol:")} @@ -157,9 +195,9 @@ const MainCard = (props: MainCardProps) => { )} - - - +
+
+
); }; 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 4166235e2..46468400b 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 @@ -130,19 +130,19 @@ export const scrambleConfig = { playOnMount: true, }; -export const useLabelScrambler = (value: string, suffix: string = "") => { - // Ignore all characters in the suffix, as long as they are not numbers. +export const useLabelScrambler = (value: string, suffix: string = "", prefix: string = "") => { + // Ignore all characters in the prefix and the suffix, as long as they are not numbers. const ignore = ["."]; const numberSet = new Set("0123456789"); - const suffixSet = new Set(suffix); - for (const char of suffixSet) { + const suffixesAndPrefixes = new Set(prefix + suffix); + for (const char of suffixesAndPrefixes) { if (!numberSet.has(char)) { ignore.push(char); } } const scrambler = useScramble({ - text: value + suffix, + text: prefix + value + suffix, ...scrambleConfig, ignore, }); diff --git a/src/typescript/frontend/src/components/price-feed/index.tsx b/src/typescript/frontend/src/components/price-feed/index.tsx index e90f224e8..dca5403df 100644 --- a/src/typescript/frontend/src/components/price-feed/index.tsx +++ b/src/typescript/frontend/src/components/price-feed/index.tsx @@ -1,6 +1,6 @@ import type { DatabaseModels } from "@sdk/indexer-v2/types"; import { PriceFeedInner } from "./inner"; -export const PriceFeed = async ({ data }: { data: Array }) => { - return ; -}; +export const PriceFeed = async ({ data }: { data: Array }) => ( + +); diff --git a/src/typescript/frontend/src/components/price-feed/inner.tsx b/src/typescript/frontend/src/components/price-feed/inner.tsx index 932bc5741..f2d4cf385 100644 --- a/src/typescript/frontend/src/components/price-feed/inner.tsx +++ b/src/typescript/frontend/src/components/price-feed/inner.tsx @@ -1,34 +1,62 @@ "use client"; -import type { fetchPriceFeed } from "@/queries/home"; import Link from "next/link"; import Carousel from "components/carousel"; import { Emoji } from "utils/emoji"; +import { useLabelScrambler } from "components/pages/home/components/table-card/animation-variants/event-variants"; +import { useMemo } from "react"; +import { cn } from "lib/utils/class-name"; +import useEffectOnce from "react-use/lib/useEffectOnce"; +import { useEventStore } from "context/event-store-context/hooks"; +import { type DatabaseModels } from "@sdk/indexer-v2/types"; -const Item = ({ emoji, change }: { emoji: string; change: number }) => { +export const PriceDelta = ({ delta, className = "" }: { delta: number; className?: string }) => { + const { prefix, suffix, text } = useMemo( + () => ({ + prefix: delta >= 0 ? "+" : "-", + suffix: "%", + text: Math.abs(delta).toFixed(2), + }), + [delta] + ); + const { ref } = useLabelScrambler(text, suffix, prefix); + + return ( + = 0 ? "text-green" : "text-pink"}`, className)}> + {`${prefix} ${text}${suffix}`} + + ); +}; + +const Item = ({ emoji, delta }: { emoji: string; delta: number }) => { return ( = 0 ? "border-green" : "border-pink"} rounded-full px-3 py-[2px] select-none mr-[22px]`} + className={`font-pixelar whitespace-nowrap border-[1px] border-solid ${delta >= 0 ? "border-green" : "border-pink"} rounded-full px-3 py-[2px] select-none mr-[22px]`} draggable={false} > - = 0 ? "text-green" : "text-pink"}`}> - {change >= 0 ? "+" : "-"} {Math.abs(change).toFixed(2)}% - + ); }; -export const PriceFeedInner = ({ data }: { data: Awaited> }) => { +export const PriceFeedInner = ({ data }: { data: DatabaseModels["price_feed"][] }) => { + // Load the price feed market data into the event store. + const loadEventsFromServer = useEventStore((s) => s.loadEventsFromServer); + + useEffectOnce(() => { + loadEventsFromServer(data); + }); + return (
{data!.map((itemData, i) => ( ))} diff --git a/src/typescript/frontend/src/lib/queries/sorting/types.ts b/src/typescript/frontend/src/lib/queries/sorting/types.ts index 8af6c0a88..29c2d822e 100644 --- a/src/typescript/frontend/src/lib/queries/sorting/types.ts +++ b/src/typescript/frontend/src/lib/queries/sorting/types.ts @@ -1,4 +1,4 @@ -import { SortMarketsBy } from "@sdk/indexer-v2/types/common"; +import { DEFAULT_SORT_BY, SortMarketsBy } from "@sdk/indexer-v2/types/common"; import { type ORDER_BY } from "@sdk/queries/const"; import { type ValueOf } from "@sdk/utils/utility-types"; @@ -93,7 +93,7 @@ export const toMarketDataSortBy = ( if (sortBy === sortByFilters[key]?.forPageQueryParams) return key as SortMarketsBy; if (sortBy === key) return key as SortMarketsBy; } - return SortMarketsBy.MarketCap; + return DEFAULT_SORT_BY; }; export const toMarketDataSortByHomePage = ( @@ -104,5 +104,5 @@ export const toMarketDataSortByHomePage = ( if (sort === SortMarketsBy.BumpOrder) return sort; if (sort === SortMarketsBy.DailyVolume) return sort; if (sort === SortMarketsBy.AllTimeVolume) return sort; - return SortMarketsBy.MarketCap; + return DEFAULT_SORT_BY; }; diff --git a/src/typescript/package.json b/src/typescript/package.json index e7a879103..963d0b3e1 100644 --- a/src/typescript/package.json +++ b/src/typescript/package.json @@ -47,7 +47,9 @@ "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" + "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", + "test:unit": "NO_TEST_SETUP=true pnpm run load-env:test -- turbo run test:unit --force", + "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/src/client/emojicoin-client.ts b/src/typescript/sdk/src/client/emojicoin-client.ts index 98fbc7090..c37dd17a9 100644 --- a/src/typescript/sdk/src/client/emojicoin-client.ts +++ b/src/typescript/sdk/src/client/emojicoin-client.ts @@ -97,6 +97,11 @@ const waitForEventProcessed = async ( export class EmojicoinClient { public aptos: Aptos; + public register = this.registerInternal.bind(this); + public chat = this.chatInternal.bind(this); + public buy = this.buyInternal.bind(this); + public sell = this.sellInternal.bind(this); + public liquidity = { provide: this.provideLiquidity.bind(this), remove: this.removeLiquidity.bind(this), @@ -161,14 +166,24 @@ export class EmojicoinClient { this.minOutputAmount = BigInt(minOutputAmount); } - async register(registrant: Account, symbolEmojis: SymbolEmoji[], options?: Options) { + // Internal so we can bind the public functions. + private async registerInternal( + registrant: Account, + symbolEmojis: SymbolEmoji[], + transactionOptions?: Options + ) { + const { feePayer, waitForTransactionOptions, options } = transactionOptions ?? {}; const response = await RegisterMarket.submit({ aptosConfig: this.aptos.config, registrant, emojis: this.emojisToHexStrings(symbolEmojis), integrator: this.integrator, - options: DEFAULT_REGISTER_MARKET_GAS_OPTIONS, - ...options, + options: { + ...DEFAULT_REGISTER_MARKET_GAS_OPTIONS, + ...options, + }, + feePayer, + waitForTransactionOptions, }); const res = this.getTransactionEventData(response); const marketID = res.events.marketRegistrationEvents[0].marketID; @@ -182,7 +197,8 @@ export class EmojicoinClient { }; } - async chat( + // Internal so we can bind the public functions. + private async chatInternal( user: Account, symbolEmojis: SymbolEmoji[], message: string | (SymbolEmoji | ChatEmoji)[], @@ -213,7 +229,8 @@ export class EmojicoinClient { }; } - async buy( + // Internal so we can bind the public functions. + private async buyInternal( swapper: Account, symbolEmojis: SymbolEmoji[], inputAmount: bigint | number, @@ -233,7 +250,8 @@ export class EmojicoinClient { ); } - async sell( + // Internal so we can bind the public functions. + private async sellInternal( swapper: Account, symbolEmojis: SymbolEmoji[], inputAmount: bigint | number, 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 c99f819d0..25ef17e8a 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/app/home.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/app/home.ts @@ -3,8 +3,8 @@ if (process.env.NODE_ENV !== "test") { } import { LIMIT, ORDER_BY } from "../../../queries/const"; -import { SortMarketsBy, type MarketStateQueryArgs } from "../../types/common"; -import { DatabaseRpc, TableName } from "../../types/json-types"; +import { DEFAULT_SORT_BY, type MarketStateQueryArgs } from "../../types/common"; +import { type DatabaseJsonType, TableName } from "../../types/json-types"; import { postgrest, toQueryArray } from "../client"; import { getLatestProcessedEmojicoinVersion, queryHelper, queryHelperWithCount } from "../utils"; import { DatabaseTypeConverter } from "../../types"; @@ -14,18 +14,26 @@ import { toRegistryView } from "../../../types"; import { sortByWithFallback } from "../query-params"; import { type PostgrestFilterBuilder } from "@supabase/postgrest-js"; -const selectMarketStates = ({ +// A helper function to abstract the logic for fetching rows that contain market state. +const selectMarketHelper = ({ + tableName, page = 1, pageSize = LIMIT, orderBy = ORDER_BY.DESC, searchEmojis, - sortBy = SortMarketsBy.MarketCap, + sortBy = DEFAULT_SORT_BY, inBondingCurve, 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); + /* eslint-disable @typescript-eslint/no-explicit-any */ +}: MarketStateQueryArgs & { tableName: T }): PostgrestFilterBuilder< + any, + any, + any[], + TableName, + T +> => { + let query: any = postgrest.from(tableName); + /* eslint-enable @typescript-eslint/no-explicit-any */ if (count === true) { query = query.select("*", { count: "exact" }); @@ -48,6 +56,15 @@ const selectMarketStates = ({ return query; }; +const selectMarketStates = (args: MarketStateQueryArgs) => + selectMarketHelper({ ...args, tableName: TableName.MarketState }); + +const selectMarketsFromPriceFeed = ({ ...args }: MarketStateQueryArgs) => + selectMarketHelper({ + ...args, + tableName: TableName.PriceFeed, + }); + export const fetchMarkets = queryHelper( selectMarketStates, DatabaseTypeConverter[TableName.MarketState] @@ -58,14 +75,6 @@ export const fetchMarketsWithCount = queryHelperWithCount( DatabaseTypeConverter[TableName.MarketState] ); -// The featured market is simply the current highest daily volume market. -export const fetchFeaturedMarket = async () => - fetchMarkets({ - page: 1, - pageSize: 1, - sortBy: SortMarketsBy.DailyVolume, - }).then((markets) => (markets ?? []).at(0)); - /** * Retrieves the number of markets by querying the view function in the registry contract on-chain. * The ledger (transaction) version is specified in order to reflect the exact total number of @@ -102,9 +111,14 @@ export const fetchNumRegisteredMarkets = async () => { } }; -const selectPriceFeed = () => postgrest.rpc(DatabaseRpc.PriceFeed, undefined, { get: true }); - -export const fetchPriceFeed = queryHelper( - selectPriceFeed, - DatabaseTypeConverter[DatabaseRpc.PriceFeed] +// Note the no-op conversion function- this is simply to satisfy the `queryHelper` params and +// indicate with generics that we don't convert the type here. +// We don't do it because of the issues with serialization/deserialization in `unstable_cache`. +// It's easier to use the conversion function later (after the response is returned from +// `unstable_cache`) rather than deal with the headache of doing it before. +// Otherwise things like `Date` objects aren't properly created upon retrieval from the +// `unstable_cache` query. +export const fetchPriceFeedWithMarketState = queryHelper( + selectMarketsFromPriceFeed, + (v): DatabaseJsonType["price_feed"] => v ); diff --git a/src/typescript/sdk/src/indexer-v2/types/common.ts b/src/typescript/sdk/src/indexer-v2/types/common.ts index 25d75c549..4f4ed1679 100644 --- a/src/typescript/sdk/src/indexer-v2/types/common.ts +++ b/src/typescript/sdk/src/indexer-v2/types/common.ts @@ -12,6 +12,8 @@ export enum SortMarketsBy { Tvl = "tvl", } +export const DEFAULT_SORT_BY = SortMarketsBy.BumpOrder; + export type MarketStateQueryArgs = { sortBy?: SortMarketsBy; page?: number; diff --git a/src/typescript/sdk/src/indexer-v2/types/index.ts b/src/typescript/sdk/src/indexer-v2/types/index.ts index fe7a58f56..8ec5a42a3 100644 --- a/src/typescript/sdk/src/indexer-v2/types/index.ts +++ b/src/typescript/sdk/src/indexer-v2/types/index.ts @@ -317,8 +317,8 @@ export type MarketDailyVolumeModel = ReturnType export type Market1MPeriodsInLastDayModel = ReturnType; export type MarketStateModel = ReturnType; export type ProcessorStatusModel = ReturnType; +export type PriceFeedModel = ReturnType; export type UserPoolsRPCModel = ReturnType; -export type PriceFeedRPCModel = ReturnType; /** * Converts a function that converts a type to another type into a function that converts the type @@ -584,19 +584,17 @@ export const toUserPoolsRPCResponse = (data: DatabaseJsonType["user_pools"]) => dailyVolume: BigInt(data.daily_volume), }); -const q64ToBigInt = (n: string) => BigInt(Big(n).div(Big(2).pow(64)).toFixed(0)); +export const calculateDeltaPercentageForQ64s = (open: AnyNumberString, close: AnyNumberString) => + q64ToBig(close.toString()).div(q64ToBig(open.toString())).mul(100).sub(100).toNumber(); -export const toPriceFeedRPCResponse = (data: DatabaseJsonType["price_feed"]) => ({ - marketID: BigInt(data.market_id), - symbolBytes: data.symbol_bytes, - symbolEmojis: data.symbol_emojis, - marketAddress: data.market_address, - openPrice: q64ToBigInt(data.open_price_q64), - closePrice: q64ToBigInt(data.close_price_q64), - deltaPercentage: Number( - q64ToBig(data.close_price_q64).div(q64ToBig(data.open_price_q64)).mul(100).sub(100).toString() - ), -}); +export const toPriceFeed = (data: DatabaseJsonType["price_feed"]) => { + return { + ...toMarketStateModel(data), + openPrice: q64ToBig(data.open_price_q64).toNumber(), + closePrice: q64ToBig(data.close_price_q64).toNumber(), + deltaPercentage: calculateDeltaPercentageForQ64s(data.open_price_q64, data.close_price_q64), + }; +}; export const DatabaseTypeConverter = { [TableName.GlobalStateEvents]: toGlobalStateEventModel, @@ -611,8 +609,8 @@ export const DatabaseTypeConverter = { [TableName.Market1MPeriodsInLastDay]: toMarket1MPeriodsInLastDay, [TableName.MarketState]: toMarketStateModel, [TableName.ProcessorStatus]: toProcessorStatus, + [TableName.PriceFeed]: toPriceFeed, [DatabaseRpc.UserPools]: toUserPoolsRPCResponse, - [DatabaseRpc.PriceFeed]: toPriceFeedRPCResponse, }; export type DatabaseModels = { @@ -628,8 +626,8 @@ export type DatabaseModels = { [TableName.Market1MPeriodsInLastDay]: Market1MPeriodsInLastDayModel; [TableName.MarketState]: MarketStateModel; [TableName.ProcessorStatus]: ProcessorStatusModel; + [TableName.PriceFeed]: PriceFeedModel; [DatabaseRpc.UserPools]: UserPoolsRPCModel; - [DatabaseRpc.PriceFeed]: PriceFeedRPCModel; }; export type AnyEventTable = 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 5ba06dc43..bd2c1b3f0 100644 --- a/src/typescript/sdk/src/indexer-v2/types/json-types.ts +++ b/src/typescript/sdk/src/indexer-v2/types/json-types.ts @@ -282,6 +282,7 @@ export enum TableName { Market1MPeriodsInLastDay = "market_1m_periods_in_last_day", MarketState = "market_state", ProcessorStatus = "processor_status", + PriceFeed = "price_feed", } export enum DatabaseRpc { @@ -351,6 +352,12 @@ export type DatabaseJsonType = { last_updated: PostgresTimestamp; last_transaction_timestamp: PostgresTimestamp; }; + [TableName.PriceFeed]: Flatten< + DatabaseJsonType["market_state"] & { + open_price_q64: Uint64String; + close_price_q64: Uint64String; + } + >; [DatabaseRpc.UserPools]: Flatten< TransactionMetadata & MarketAndStateMetadata & @@ -358,12 +365,6 @@ export type DatabaseJsonType = { ProcessedFields & UserLPCoinBalance & { daily_volume: Uint128String } >; - [DatabaseRpc.PriceFeed]: Flatten< - Omit & { - open_price_q64: Uint64String; - close_price_q64: Uint64String; - } - >; }; type Columns = DatabaseJsonType[TableName.GlobalStateEvents] & @@ -377,7 +378,9 @@ type Columns = DatabaseJsonType[TableName.GlobalStateEvents] & DatabaseJsonType[TableName.MarketDailyVolume] & DatabaseJsonType[TableName.Market1MPeriodsInLastDay] & DatabaseJsonType[TableName.MarketState] & + DatabaseJsonType[TableName.PriceFeed] & DatabaseJsonType[TableName.ProcessorStatus] & - DatabaseJsonType[DatabaseRpc.UserPools]; + DatabaseJsonType[DatabaseRpc.UserPools] & + DatabaseJsonType[DatabaseRpc.PriceFeed]; export type AnyColumnName = keyof Columns; diff --git a/src/typescript/sdk/src/utils/nominal-price.ts b/src/typescript/sdk/src/utils/nominal-price.ts index 001f0e0c9..81b7ccf06 100644 --- a/src/typescript/sdk/src/utils/nominal-price.ts +++ b/src/typescript/sdk/src/utils/nominal-price.ts @@ -2,7 +2,7 @@ import { Big } from "big.js"; /* eslint-disable-next-line no-bitwise */ -const Q64_BASE = Big((1n << 64n).toString()); +const Q64_BASE = Big(Big(2).pow(64).toString()); const DECIMALS = 16; export const q64ToBig = (q64: string | number | bigint) => Big(q64.toString()).div(Q64_BASE); @@ -16,3 +16,5 @@ export const toQuotePrice = ( avgExecutionPriceQ64: string | number | bigint, decimals: number = DECIMALS ) => Number(Big(Q64_BASE).div(avgExecutionPriceQ64.toString()).toFixed(decimals)); + +export const toQ64Big = (input: string | number | bigint) => Big(input.toString()).mul(Q64_BASE); diff --git a/src/typescript/sdk/tests/e2e/queries/price-feed.test.ts b/src/typescript/sdk/tests/e2e/queries/price-feed.test.ts new file mode 100644 index 000000000..42ceb2763 --- /dev/null +++ b/src/typescript/sdk/tests/e2e/queries/price-feed.test.ts @@ -0,0 +1,113 @@ +import { + fetchPriceFeedWithMarketState, + waitForEmojicoinIndexer, +} from "../../../src/indexer-v2/queries"; +import { getFundedAccount } from "../../../src/utils/test/test-accounts"; +import { EmojicoinClient } from "../../../src/client/emojicoin-client"; +import { + type AnyNumberString, + compareBigInt, + maxBigInt, + type SymbolEmoji, + toSequenceNumberOptions, +} from "../../../src"; +import { calculateDeltaPercentageForQ64s, toPriceFeed } from "../../../src/indexer-v2/types"; +import { SortMarketsBy } from "../../../src/indexer-v2/types/common"; +import { ORDER_BY } from "../../../src/queries"; +import Big from "big.js"; + +jest.setTimeout(30000); + +const percentageOfInputToBigInt = (amount: AnyNumberString, percentage: number) => + BigInt( + Big(amount.toString()) + .mul(1 + percentage) + .round(0, Big.roundDown) + .toNumber() + ); + +describe("queries price_feed and returns accurate price feed data", () => { + it("checks that the price feed has correct market data", async () => { + const acc = getFundedAccount("073"); + const emojicoin = new EmojicoinClient({ integratorFeeRateBPs: 0 }); + const emojisAndInputAmounts: [SymbolEmoji[], bigint, number][] = [ + // emoji, buy amount, second amount (percentage of output from buy) + // Note the percentage here doesn't indicate the delta percentage, since that's determined + // by the output average execution price, not the input amount. + [["🧘"], 10000n, -0.75], + [["🧘🏻"], 1000n, 0.9], + [["🧘🏼"], 1000n, 1], + [["🧘🏽"], 1000n, -0.05], + [["🧘🏾"], 1000n, -0.25], + [["🧘🏿"], 500n, 0.25], + ]; + const results = await Promise.all( + emojisAndInputAmounts.map(([emojis, buyAmount, percentOfOutput], i) => + emojicoin.register(acc, emojis, toSequenceNumberOptions(i * 3 + 0)).then(() => + emojicoin + .buy(acc, emojis, buyAmount, toSequenceNumberOptions(i * 3 + 1)) + .then(({ swap: openSwap }) => + (percentOfOutput > 0 ? emojicoin.buy : emojicoin.sell)( + acc, + emojis, + percentageOfInputToBigInt(openSwap.model.swap.netProceeds, percentOfOutput), + toSequenceNumberOptions(i * 3 + 2) + ).then(({ swap: closeSwap }) => ({ + openSwap, + closeSwap, + })) + ) + ) + ) + ); + + const maxTransactionVersion = maxBigInt( + ...results.map(({ closeSwap }) => closeSwap.model.transaction.version) + ); + await waitForEmojicoinIndexer(maxTransactionVersion); + + const limit = 500; + const priceFeedView = await fetchPriceFeedWithMarketState({ + sortBy: SortMarketsBy.DailyVolume, + orderBy: ORDER_BY.DESC, + pageSize: limit, + }).then((v) => v.map(toPriceFeed)); + + // If the # of rows returned is >= `limit`, this test may fail, so ensure it fails here. + expect(priceFeedView.length).toBeLessThan(limit); + + // Ensure it sorts by daily volume. Note that we cannot check the market IDs here, because + // postgres sort order is non-deterministic when returning rows of equal values. + const dailyVolumeFromView = priceFeedView.map((v) => v.dailyVolume); + const dailyVolumeSortedManually = priceFeedView + .toSorted((a, b) => compareBigInt(a.dailyVolume, b.dailyVolume)) + .reverse() // Sort by daily volume *descending*. + .map((v) => v.dailyVolume); + expect(dailyVolumeFromView).toEqual(dailyVolumeSortedManually); + + // Ensure the prices returned are expected. + results.forEach(({ openSwap, closeSwap }) => { + const symbolEmojis = openSwap.model.market.symbolEmojis; + expect(closeSwap.model.market.symbolEmojis).toEqual(symbolEmojis); + const [_, firstAmount, percentageOfFirst] = emojisAndInputAmounts.find( + ([emojis, _swap1, _swap2]) => emojis.join("") == symbolEmojis.join("") + )!; + expect(firstAmount).toBeDefined(); + expect(percentageOfFirst).toBeDefined(); + expect(firstAmount).toEqual(openSwap.model.swap.inputAmount); + expect(closeSwap.model.swap.inputAmount).toEqual( + percentageOfInputToBigInt(openSwap.model.swap.netProceeds, percentageOfFirst) + ); + const [open, close] = [ + openSwap.model.swap.avgExecutionPriceQ64, + closeSwap.model.swap.avgExecutionPriceQ64, + ]; + const expectedPercentage = calculateDeltaPercentageForQ64s(open, close); + const rowInView = priceFeedView.find( + (v) => v.market.symbolEmojis.join("") === symbolEmojis.join("") + )!; + expect(rowInView).toBeDefined(); + expect(rowInView.deltaPercentage).toEqual(expectedPercentage); + }); + }); +}); 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 deleted file mode 100644 index b9b7bbc5e..000000000 --- a/src/typescript/sdk/tests/e2e/queries/price-feed/price-feed.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 0d7d33e25..000000000 --- a/src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_current_day_swap.sql +++ /dev/null @@ -1,52 +0,0 @@ --- 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 deleted file mode 100644 index 5cd41f204..000000000 --- a/src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_market_state.sql +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 9e432e3f5..000000000 --- a/src/typescript/sdk/tests/e2e/queries/price-feed/test_1_insert_past_day_swap.sql +++ /dev/null @@ -1,52 +0,0 @@ --- 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 deleted file mode 100644 index c330c73ea..000000000 --- a/src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_earlier_swap.sql +++ /dev/null @@ -1,52 +0,0 @@ --- 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 deleted file mode 100644 index 6ace1528b..000000000 --- a/src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_later_swap.sql +++ /dev/null @@ -1,52 +0,0 @@ --- 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 deleted file mode 100644 index b5ba6213f..000000000 --- a/src/typescript/sdk/tests/e2e/queries/price-feed/test_2_insert_market_state.sql +++ /dev/null @@ -1,44 +0,0 @@ -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 -) diff --git a/src/typescript/sdk/tests/unit/delta-percentage.test.ts b/src/typescript/sdk/tests/unit/delta-percentage.test.ts new file mode 100644 index 000000000..90a0f632c --- /dev/null +++ b/src/typescript/sdk/tests/unit/delta-percentage.test.ts @@ -0,0 +1,29 @@ +import { calculateDeltaPercentageForQ64s } from "../../src/indexer-v2/types"; +import { toQ64Big } from "../../src/utils/nominal-price"; + +describe("tests the delta percentage calculations", () => { + it("calculates the delta percentage accurately", () => { + const inputs = [ + [100, 500, 400], + [100, 50, -50], + [100, 10, -90], + [100, 1, -99], + [100, 0.1, -99.9], + [100, 0.01, -99.99], + [1, 10, 900], + [1, 16, 1500], + [10, 160, 1500], + [160, 10, -93.75], + [16, 1, -93.75], + [100, 175, 75], + [100, 75, -25], + [100, 25, -75], + ]; + + for (const [open, close, expectedPercentage] of inputs) { + const [openQ64, closeQ64] = [toQ64Big(open).toString(), toQ64Big(close).toString()]; + const receivedPercentage = calculateDeltaPercentageForQ64s(openQ64, closeQ64); + expect(receivedPercentage).toEqual(expectedPercentage); + } + }); +}); diff --git a/src/typescript/turbo.json b/src/typescript/turbo.json index 2b7b99883..76462bf5d 100644 --- a/src/typescript/turbo.json +++ b/src/typescript/turbo.json @@ -81,7 +81,7 @@ "cache": false, "outputs": [] }, - "test:sdk:unit": { + "test:unit": { "cache": false, "outputs": [] }