diff --git a/src/docker/compose.yaml b/src/docker/compose.yaml index ef320a385..baae8fd15 100644 --- a/src/docker/compose.yaml +++ b/src/docker/compose.yaml @@ -15,7 +15,7 @@ services: PROCESSOR_WS_URL: 'ws://processor:${PROCESSOR_WS_PORT}/ws' PORT: '${BROKER_PORT}' RUST_LOG: 'info,broker=trace' - image: 'econialabs/emojicoin-dot-fun-indexer-broker' + image: 'econialabs/emojicoin-dot-fun-indexer-broker:6.0.0' container_name: 'broker' healthcheck: test: 'curl -f http://localhost:${BROKER_PORT}/live || exit 1' @@ -83,7 +83,7 @@ services: depends_on: postgres: condition: 'service_healthy' - image: 'econialabs/emojicoin-dot-fun-indexer-processor' + image: 'econialabs/emojicoin-dot-fun-indexer-processor:6.0.0' container_name: 'processor' healthcheck: test: 'curl -sf http://localhost:${PROCESSOR_WS_PORT} || exit 1' diff --git a/src/rust/processor b/src/rust/processor index 8e0fe45ab..a32ed5ec8 160000 --- a/src/rust/processor +++ b/src/rust/processor @@ -1 +1 @@ -Subproject commit 8e0fe45ab9cc089ca14cc6bec7a66b16f3eef05b +Subproject commit a32ed5ec82a134e9b264315e2c0b9f1fb4a4b912 diff --git a/src/typescript/frontend/src/components/pages/arena/tabs/EnterTab.tsx b/src/typescript/frontend/src/components/pages/arena/tabs/EnterTab.tsx index 94a7d8158..fd92f0030 100644 --- a/src/typescript/frontend/src/components/pages/arena/tabs/EnterTab.tsx +++ b/src/typescript/frontend/src/components/pages/arena/tabs/EnterTab.tsx @@ -11,7 +11,7 @@ import { FormattedNumber } from "components/FormattedNumber"; import { useAptos } from "context/wallet-context/AptosContextProvider"; import ButtonWithConnectWalletFallback from "components/header/wallet-button/ConnectWalletButton"; import { CloseIcon } from "components/svg"; -import type { UserTransactionResponse } from "@aptos-labs/ts-sdk"; +import { isUserTransactionResponse, type UserTransactionResponse } from "@aptos-labs/ts-sdk"; import { ARENA_MODULE_ADDRESS } from "@sdk/const"; import { emoji } from "utils"; import { q64ToBig } from "@sdk/utils"; @@ -220,23 +220,27 @@ const EnterTabLockPhase: React.FC<{ scale="lg" onClick={() => { if (!account) return; - submit(transactionBuilder).then((r) => { - if (!r || r?.error) { + submit(transactionBuilder).then((res) => { + if (!res || res?.error) { errorOut(); } else if ( - (r.response as UserTransactionResponse).events.find( + (res.response as UserTransactionResponse).events.find( (e) => e.type === `${ARENA_MODULE_ADDRESS}::emojicoin_arena::Melee` ) ) { setCranked(); } else { - const enterEvent = (r.response as UserTransactionResponse).events.find( + const { response } = res; + if (!response || !isUserTransactionResponse(response)) return; + const version = BigInt(response.version); + const enterEvent = response.events.find( (e) => e.type === `${ARENA_MODULE_ADDRESS}::emojicoin_arena::Enter` )!; - + if (!enterEvent) return; if (position && position.open) { setPosition({ ...position, + version, deposits: position.deposits + BigInt(enterEvent.data.input_amount), matchAmount: position.matchAmount + BigInt(enterEvent.data.match_amount), emojicoin0Balance: @@ -247,6 +251,7 @@ const EnterTabLockPhase: React.FC<{ } else { setPosition({ open: true, + version, user: account.address as `0x${string}`, meleeID: BigInt(enterEvent.data.melee_id), deposits: BigInt(enterEvent.data.input_amount), diff --git a/src/typescript/package.json b/src/typescript/package.json index 7a0692190..35284a01f 100644 --- a/src/typescript/package.json +++ b/src/typescript/package.json @@ -45,6 +45,7 @@ "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:arena": "pnpm run load-env:test -- turbo run test:sdk:arena --filter @econia-labs/emojicoin-sdk --log-prefix none", + "test:sdk:arena-candlesticks": "pnpm run load-env:test -- turbo run test:sdk:arena-candlesticks --filter @econia-labs/emojicoin-sdk --log-prefix none", "test:sdk:e2e": "pnpm run load-env:test -- turbo run test:sdk: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", diff --git a/src/typescript/sdk/package.json b/src/typescript/sdk/package.json index aa7bb3af5..41911a324 100644 --- a/src/typescript/sdk/package.json +++ b/src/typescript/sdk/package.json @@ -111,8 +111,10 @@ "prepublishOnly": "pnpm clean && pnpm i && pnpm check && pnpm build:publish", "test": "pnpm run test:sdk:arena && pnpm run test:sdk:arena-ws && 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:arena": "pnpm jest tests/e2e/arena/general --runInBand", - "test:sdk:arena-ws": "pnpm jest tests/e2e/arena/ws --runInBand", + "test:sdk:arena": "pnpm run test:sdk:arena-candlesticks && pnpm run test:sdk:arena-general && pnpm run test:sdk:arena-ws", + "test:sdk:arena-general": "pnpm jest tests/e2e/arena/general.test.ts --runInBand", + "test:sdk:arena-candlesticks": "pnpm jest tests/e2e/arena/candlesticks.test.ts --runInBand", + "test:sdk:arena-ws": "pnpm jest tests/e2e/arena/websockets.test.ts --runInBand", "test:sdk:parallel": "pnpm jest tests/e2e --testPathIgnorePatterns=\"(arena/|broker/)\"", "test:sdk:sequential": "pnpm jest tests/e2e/broker --runInBand", "test:unit": "pnpm jest tests/unit", diff --git a/src/typescript/sdk/src/const.ts b/src/typescript/sdk/src/const.ts index 792adb6a6..63c25ed72 100644 --- a/src/typescript/sdk/src/const.ts +++ b/src/typescript/sdk/src/const.ts @@ -154,7 +154,6 @@ export const INITIAL_REAL_RESERVES: Types["Reserves"] = { quote: 0n, }; -/// As defined in the database, aka the enum string. export enum Period { Period1M = "period_1m", Period5M = "period_5m", @@ -165,6 +164,17 @@ export enum Period { Period1D = "period_1d", } +export enum ArenaPeriod { + Period15S = "period_15s", + Period1M = "period_1m", + Period5M = "period_5m", + Period15M = "period_15m", + Period30M = "period_30m", + Period1H = "period_1h", + Period4H = "period_4h", + Period1D = "period_1d", +} + /// As defined in the database, aka the enum string. export enum Trigger { PackagePublication = "package_publication", @@ -199,6 +209,31 @@ export const toPeriod = (s: DatabaseStructType["PeriodicStateMetadata"]["period" throw new Error(`Unknown period: ${s}`); })(); +export const toArenaPeriod = (s: DatabaseStructType["ArenaCandlestick"]["period"]) => + ({ + // From the database. + period_15s: ArenaPeriod.Period15S, + period_1m: ArenaPeriod.Period1M, + period_5m: ArenaPeriod.Period5M, + period_15m: ArenaPeriod.Period15M, + period_30m: ArenaPeriod.Period30M, + period_1h: ArenaPeriod.Period1H, + period_4h: ArenaPeriod.Period4H, + period_1d: ArenaPeriod.Period1D, + // From the broker. + FifteenSeconds: ArenaPeriod.Period15S, + OneMinute: ArenaPeriod.Period1M, + FiveMinutes: ArenaPeriod.Period5M, + FifteenMinutes: ArenaPeriod.Period15M, + ThirtyMinutes: ArenaPeriod.Period30M, + OneHour: ArenaPeriod.Period1H, + FourHours: ArenaPeriod.Period4H, + OneDay: ArenaPeriod.Period1D, + })[s as ValueOf] ?? + (() => { + throw new Error(`Unknown period: ${s}`); + })(); + export const toTrigger = (s: DatabaseStructType["GlobalStateEventData"]["trigger"]) => ({ // From the database. diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/arena.ts b/src/typescript/sdk/src/indexer-v2/queries/app/arena.ts index 55597a99c..848a9dc1a 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/app/arena.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/app/arena.ts @@ -14,6 +14,7 @@ import { } from "../../types"; import { ORDER_BY } from "../../const"; import { toAccountAddressString } from "../../../utils/account-address"; +import { type AccountAddressInput } from "@aptos-labs/ts-sdk"; const selectMelee = () => postgrest @@ -31,19 +32,19 @@ const selectArenaInfo = () => .limit(1) .single(); -const selectPosition = ({ user, meleeID }: { user: string; meleeID: bigint }) => +const selectPosition = ({ user, meleeID }: { user: AccountAddressInput; meleeID: bigint }) => postgrest .from(TableName.ArenaPosition) .select("*") - .eq("user", user) + .eq("user", toAccountAddressString(user)) .eq("melee_id", meleeID) .maybeSingle(); -const selectLatestPosition = ({ user }: { user: string }) => +const selectLatestPosition = ({ user }: { user: AccountAddressInput }) => postgrest .from(TableName.ArenaPosition) .select("*") - .eq("user", user) + .eq("user", toAccountAddressString(user)) .order("melee_id", ORDER_BY.DESC) .limit(1) .maybeSingle(); @@ -53,7 +54,7 @@ const selectArenaLeaderboardHistoryWithInfo = ({ page = 1, pageSize, }: { - user: string; + user: AccountAddressInput; page: number; pageSize: number; }) => @@ -69,7 +70,7 @@ const selectMarketStateByAddress = ({ address }: { address: string }) => postgrest .from(TableName.MarketState) .select("*") - .eq("market_address", address) + .eq("market_address", toAccountAddressString(address)) .limit(1) .maybeSingle(); diff --git a/src/typescript/sdk/src/indexer-v2/types/index.ts b/src/typescript/sdk/src/indexer-v2/types/index.ts index 6272438e9..8a4875908 100644 --- a/src/typescript/sdk/src/indexer-v2/types/index.ts +++ b/src/typescript/sdk/src/indexer-v2/types/index.ts @@ -25,11 +25,15 @@ import { type BlockAndEventIndexMetadata, } from "./json-types"; import { type MarketEmojiData, type SymbolEmoji, toMarketEmojiData } from "../../emoji_data"; -import { toPeriod, toTrigger, type Period, type Trigger } from "../../const"; +import { toArenaPeriod, toPeriod, toTrigger, type Period, type Trigger } from "../../const"; import { toAccountAddressString } from "../../utils"; import Big from "big.js"; import { q64ToBig } from "../../utils/nominal-price"; -import { type AnyArenaEvent } from "../../types/arena-types"; +import { + ARENA_CANDLESTICK_NAME, + safeParseBigIntOrPostgresTimestamp, + type AnyArenaEvent, +} from "../../types/arena-types"; import { calculateCurvePrice, type ReservesAndBondingCurveState } from "../../markets"; export type TransactionMetadata = { @@ -158,6 +162,7 @@ const toArenaExitFromDatabase = ( emojicoin0ExchangeRateQuote: BigInt(data.emojicoin_0_exchange_rate_quote), emojicoin1ExchangeRateBase: BigInt(data.emojicoin_1_exchange_rate_base), emojicoin1ExchangeRateQuote: BigInt(data.emojicoin_1_exchange_rate_quote), + duringMelee: data.during_melee, }); const toArenaSwapFromDatabase = ( @@ -173,6 +178,7 @@ const toArenaSwapFromDatabase = ( emojicoin0ExchangeRateQuote: BigInt(data.emojicoin_0_exchange_rate_quote), emojicoin1ExchangeRateBase: BigInt(data.emojicoin_1_exchange_rate_base), emojicoin1ExchangeRateQuote: BigInt(data.emojicoin_1_exchange_rate_quote), + duringMelee: data.during_melee, }); const toArenaVaultBalanceUpdateFromDatabase = ( @@ -185,6 +191,7 @@ const toArenaPositionFromDatabase = ( data: DatabaseStructType["ArenaPosition"] ): Types["ArenaPosition"] => ({ meleeID: BigInt(data.melee_id), + version: BigInt(data.last_transaction_version), user: toAccountAddressString(data.user), open: data.open, emojicoin0Balance: BigInt(data.emojicoin_0_balance), @@ -199,6 +206,7 @@ const toArenaLeaderboardFromDatabase = ( data: DatabaseStructType["ArenaLeaderboard"] ): Types["ArenaLeaderboard"] => ({ user: toAccountAddressString(data.user), + version: BigInt(data.last_transaction_version), open: data.open, emojicoin0Balance: BigInt(data.emojicoin_0_balance), emojicoin1Balance: BigInt(data.emojicoin_1_balance), @@ -232,6 +240,7 @@ export const toTotalAptLocked = (args: { const toArenaInfoFromDatabase = (data: DatabaseStructType["ArenaInfo"]): Types["ArenaInfo"] => ({ meleeID: BigInt(data.melee_id), + version: BigInt(data.last_transaction_version), volume: BigInt(data.volume), rewardsRemaining: BigInt(data.rewards_remaining), emojicoin0Locked: BigInt(data.emojicoin_0_locked), @@ -248,10 +257,26 @@ const toArenaInfoFromDatabase = (data: DatabaseStructType["ArenaInfo"]): Types[" maxMatchAmount: BigInt(data.max_match_amount), }); +const toArenaCandlestickFromDatabase = ( + data: DatabaseStructType["ArenaCandlestick"] +): Types["ArenaCandlestick"] => ({ + meleeID: BigInt(data.melee_id), + version: BigInt(data.last_transaction_version), + volume: BigInt(data.volume), + period: toArenaPeriod(data.period), + startTime: safeParseBigIntOrPostgresTimestamp(data.start_time), + openPrice: Number(data.open_price), + closePrice: Number(data.close_price), + highPrice: Number(data.high_price), + lowPrice: Number(data.low_price), + nSwaps: BigInt(data.n_swaps), +}); + const toArenaLeaderboardHistoryFromDatabase = ( data: DatabaseStructType["ArenaLeaderboardHistory"] ): Types["ArenaLeaderboardHistory"] => ({ user: toAccountAddressString(data.user), + version: BigInt(data.last_transaction_version), meleeID: BigInt(data.melee_id), profits: BigInt(data.profits), losses: BigInt(data.losses), @@ -491,6 +516,7 @@ export type ArenaPositionModel = ReturnType; export type ArenaLeaderboardModel = ReturnType; export type ArenaLeaderboardHistoryModel = ReturnType; export type ArenaInfoModel = ReturnType; +export type ArenaCandlestickModel = ReturnType; export type UserPoolsRPCModel = ReturnType; export type AggregateMarketStateModel = ReturnType; export type ArenaLeaderboardHistoryWithArenaInfoModel = ReturnType< @@ -687,6 +713,11 @@ export const GuidGetters = { eventName: EVENT_NAMES.ArenaVaultBalanceUpdate, guid: `${EVENT_NAMES.ArenaVaultBalanceUpdate}::${sender}::${version}::${event_index}`, }), + arenaCandlestick: ({ melee_id, start_time, period }: DatabaseJsonType["arena_candlesticks"]) => ({ + // Not a real contract event, but used to classify the type of data. + eventName: ARENA_CANDLESTICK_NAME, + guid: `${ARENA_CANDLESTICK_NAME}::${melee_id}::${period}::${start_time}`, + }), }; export const toGlobalStateEventModel = (data: DatabaseJsonType["global_state_events"]) => ({ @@ -897,6 +928,10 @@ export const toArenaPositionModel = toArenaPositionFromDatabase; export const toArenaLeaderboardModel = toArenaLeaderboardFromDatabase; export const toArenaLeaderboardHistoryModel = toArenaLeaderboardHistoryFromDatabase; export const toArenaInfoModel = toArenaInfoFromDatabase; +export const toArenaCandlestickModel = (data: DatabaseJsonType["arena_candlesticks"]) => ({ + ...toArenaCandlestickFromDatabase(data), + ...GuidGetters.arenaCandlestick(data), +}); export const calculateDeltaPercentageForQ64s = (open: AnyNumberString, close: AnyNumberString) => q64ToBig(close.toString()).div(q64ToBig(open.toString())).mul(100).sub(100).toNumber(); @@ -951,6 +986,7 @@ export const DatabaseTypeConverter = { [TableName.ArenaExitEvents]: toArenaExitModel, [TableName.ArenaSwapEvents]: toArenaSwapModel, [TableName.ArenaInfo]: toArenaInfoModel, + [TableName.ArenaCandlesticks]: toArenaCandlestickModel, [TableName.ArenaPosition]: toArenaPositionModel, [TableName.ArenaVaultBalanceUpdateEvents]: toArenaVaultBalanceUpdateModel, [TableName.ArenaLeaderboard]: toArenaLeaderboardModel, @@ -981,6 +1017,7 @@ export type DatabaseModels = { [TableName.ArenaVaultBalanceUpdateEvents]: ArenaVaultBalanceUpdateModel; [TableName.ArenaPosition]: ArenaPositionModel; [TableName.ArenaInfo]: ArenaInfoModel; + [TableName.ArenaCandlesticks]: ArenaCandlestickModel; [TableName.ArenaLeaderboard]: ArenaLeaderboardModel; [TableName.ArenaLeaderboardHistory]: ArenaLeaderboardHistoryModel; [TableName.ArenaLeaderboardHistoryWithArenaInfo]: ArenaLeaderboardHistoryWithArenaInfoModel; 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 9b89b87e3..8e3f7e946 100644 --- a/src/typescript/sdk/src/indexer-v2/types/json-types.ts +++ b/src/typescript/sdk/src/indexer-v2/types/json-types.ts @@ -11,6 +11,7 @@ import type { import { type JsonTypes, type Flatten } from "../../types"; export type PeriodTypeFromDatabase = + | "period_15s" | "period_1m" | "period_5m" | "period_15m" @@ -20,6 +21,7 @@ export type PeriodTypeFromDatabase = | "period_1d"; export type PeriodTypeFromBroker = + | "FifteenSeconds" | "OneMinute" | "FiveMinutes" | "FifteenMinutes" @@ -271,8 +273,11 @@ type ArenaMeleeEventData = Flatten< type ArenaEnterEventData = FlattenedExchangeRateWithEventIndex<"ArenaEnterEvent">; type ArenaExitEventData = FlattenedExchangeRateWithEventIndex<"ArenaExitEvent"> & { apt_proceeds: Uint64String; + during_melee: boolean; +}; +type ArenaSwapEventData = FlattenedExchangeRateWithEventIndex<"ArenaSwapEvent"> & { + during_melee: boolean; }; -type ArenaSwapEventData = FlattenedExchangeRateWithEventIndex<"ArenaSwapEvent">; type ArenaVaultBalanceUpdateEventData = { event_index: Uint64String; @@ -281,6 +286,7 @@ type ArenaVaultBalanceUpdateEventData = { type ArenaPositionData = { user: AccountAddressString; + last_transaction_version: Uint64String; melee_id: Uint64String; open: boolean; emojicoin_0_balance: Uint64String; @@ -293,6 +299,7 @@ type ArenaPositionData = { type ArenaInfoData = { melee_id: Uint64String; + last_transaction_version: Uint64String; volume: Uint64String; rewards_remaining: Uint64String; emojicoin_0_locked: Uint64String; @@ -311,6 +318,7 @@ type ArenaInfoData = { type ArenaLeaderboardHistoryData = { user: AccountAddressString; + last_transaction_version: Uint64String; melee_id: Uint64String; profits: Uint64String; losses: Uint64String; @@ -321,28 +329,35 @@ type ArenaLeaderboardHistoryData = { withdrawals: Uint64String; }; -type ArenaLeaderboardHistoryWithArenaInfoData = { - user: AccountAddressString; - melee_id: Uint64String; - profits: Uint64String; - losses: Uint64String; - withdrawals: Uint64String; - emojicoin_0_balance: Uint64String; - emojicoin_1_balance: Uint64String; - exited: boolean; - last_exit_0: boolean | null; - emojicoin_0_market_address: AccountAddressString; - emojicoin_1_market_address: AccountAddressString; - emojicoin_0_market_id: Uint64String; - emojicoin_1_market_id: Uint64String; - emojicoin_0_symbols: SymbolEmoji[]; - emojicoin_1_symbols: SymbolEmoji[]; - start_time: string; - duration: Uint64String; -}; +type ArenaLeaderboardHistoryWithArenaInfoData = Flatten< + Pick< + ArenaLeaderboardHistoryData, + | "user" + | "melee_id" + | "profits" + | "losses" + | "withdrawals" + | "emojicoin_0_balance" + | "emojicoin_1_balance" + | "exited" + | "last_exit_0" + > & + Pick< + ArenaInfoData, + | "emojicoin_0_symbols" + | "emojicoin_1_symbols" + | "emojicoin_0_market_address" + | "emojicoin_1_market_address" + | "emojicoin_0_market_id" + | "emojicoin_1_market_id" + | "start_time" + | "duration" + > +>; type ArenaLeaderboardData = { user: AccountAddressString; + last_transaction_version: Uint64String; open: boolean; emojicoin_0_balance: Uint64String; emojicoin_1_balance: Uint64String; @@ -353,6 +368,21 @@ type ArenaLeaderboardData = { withdrawals: Uint64String; }; +type ArenaCandlestickData = { + melee_id: Uint64String; + last_transaction_version: Uint64String; + period: PeriodTypeFromDatabase | PeriodTypeFromBroker; + start_time: PostgresTimestamp; + + open_price: number; + close_price: number; + high_price: number; + low_price: number; + + volume: Uint64String; + n_swaps: Uint64String; +}; + export type DatabaseStructType = { TransactionMetadata: TransactionMetadata; MarketAndStateMetadata: MarketAndStateMetadata; @@ -374,6 +404,7 @@ export type DatabaseStructType = { ArenaLeaderboard: ArenaLeaderboardData; ArenaLeaderboardHistory: ArenaLeaderboardHistoryData; ArenaInfo: ArenaInfoData; + ArenaCandlestick: ArenaCandlestickData; }; export type BrokerJsonTypes = @@ -412,6 +443,7 @@ export enum TableName { ArenaVaultBalanceUpdateEvents = "arena_vault_balance_update_events", ArenaPosition = "arena_position", ArenaInfo = "arena_info", + ArenaCandlesticks = "arena_candlesticks", // The view for the current arena leaderboard, all users. ArenaLeaderboard = "arena_leaderboard", // The table for a user's historic arena pnl. @@ -513,6 +545,7 @@ export type DatabaseJsonType = { >; [TableName.ArenaPosition]: ArenaPositionData; [TableName.ArenaInfo]: ArenaInfoData; + [TableName.ArenaCandlesticks]: ArenaCandlestickData; [TableName.ArenaLeaderboard]: ArenaLeaderboardData; [TableName.ArenaLeaderboardHistory]: ArenaLeaderboardHistoryData; @@ -567,6 +600,7 @@ type Columns = DatabaseJsonType[TableName.GlobalStateEvents] & DatabaseJsonType[TableName.ArenaVaultBalanceUpdateEvents] & DatabaseJsonType[TableName.ArenaPosition] & DatabaseJsonType[TableName.ArenaInfo] & + DatabaseJsonType[TableName.ArenaCandlesticks] & DatabaseJsonType[TableName.ArenaLeaderboard] & DatabaseJsonType[TableName.ArenaLeaderboardHistory] & DatabaseJsonType[DatabaseRpc.UserPools] & diff --git a/src/typescript/sdk/src/indexer-v2/types/postgres-numeric-types.ts b/src/typescript/sdk/src/indexer-v2/types/postgres-numeric-types.ts index db30d0da2..0f3eef0fb 100644 --- a/src/typescript/sdk/src/indexer-v2/types/postgres-numeric-types.ts +++ b/src/typescript/sdk/src/indexer-v2/types/postgres-numeric-types.ts @@ -122,6 +122,10 @@ export const floatColumns: Set = new Set([ "emojicoin_0_locked", "emojicoin_1_locked", "apt_proceeds", + "open_price", + "close_price", + "high_price", + "low_price", ]); /** @@ -135,6 +139,7 @@ export const bigintColumns: Set = new Set([ "block_number", "event_index", "last_success_version", + "last_transaction_version", "transaction_version", ]); diff --git a/src/typescript/sdk/src/types/arena-types.ts b/src/typescript/sdk/src/types/arena-types.ts index 59f432335..016b21f24 100644 --- a/src/typescript/sdk/src/types/arena-types.ts +++ b/src/typescript/sdk/src/types/arena-types.ts @@ -1,3 +1,4 @@ +import type { ArenaPeriod } from "../const"; import { type SymbolEmoji } from "../emoji_data"; import { type AccountAddressString } from "../emojicoin_dot_fun"; import { @@ -13,6 +14,8 @@ import { dateFromMicroseconds, toAccountAddressString } from "../utils"; import type JsonTypes from "./json-types"; import { type AnyNumberString, type Types } from "./types"; +export const ARENA_CANDLESTICK_NAME = "ArenaCandlestick"; + type WithVersionAndEventIndex = { version: number | string; eventIndex: number; @@ -58,6 +61,7 @@ export type ArenaTypes = { emojicoin0ExchangeRateQuote: bigint; emojicoin1ExchangeRateBase: bigint; emojicoin1ExchangeRateQuote: bigint; + duringMelee: boolean; eventName: "ArenaExit"; } & WithVersionAndEventIndex; @@ -72,6 +76,7 @@ export type ArenaTypes = { emojicoin0ExchangeRateQuote: bigint; emojicoin1ExchangeRateBase: bigint; emojicoin1ExchangeRateQuote: bigint; + duringMelee: boolean; eventName: "ArenaSwap"; } & WithVersionAndEventIndex; @@ -82,6 +87,7 @@ export type ArenaTypes = { ArenaPosition: { user: AccountAddressString; + version: bigint; meleeID: bigint; open: boolean; emojicoin0Balance: bigint; @@ -94,6 +100,7 @@ export type ArenaTypes = { ArenaLeaderboardHistory: { user: AccountAddressString; + version: bigint; meleeID: bigint; profits: bigint; losses: bigint; @@ -127,6 +134,7 @@ export type ArenaTypes = { ArenaLeaderboard: { user: AccountAddressString; + version: bigint; open: boolean; emojicoin0Balance: bigint; emojicoin1Balance: bigint; @@ -139,6 +147,7 @@ export type ArenaTypes = { ArenaInfo: { meleeID: bigint; + version: bigint; volume: bigint; rewardsRemaining: bigint; emojicoin0Locked: bigint; @@ -154,6 +163,19 @@ export type ArenaTypes = { maxMatchPercentage: bigint; maxMatchAmount: bigint; }; + + ArenaCandlestick: { + meleeID: bigint; + version: bigint; + period: ArenaPeriod; + startTime: Date; + openPrice: number; + closePrice: number; + highPrice: number; + lowPrice: number; + volume: bigint; + nSwaps: bigint; + }; }; const toExchangeRate = (data: JsonTypes["BothEmojicoinExchangeRates"]) => ({ @@ -255,6 +277,7 @@ export const toArenaExitEvent = ( ...toExchangeRate(data), eventName: "ArenaExit" as const, ...withVersionAndEventIndex({ version, eventIndex }), + duringMelee: true, }); export const toArenaSwapEvent = ( @@ -271,6 +294,7 @@ export const toArenaSwapEvent = ( ...toExchangeRate(data), eventName: "ArenaSwap" as const, ...withVersionAndEventIndex({ version, eventIndex }), + duringMelee: true, }); export const toArenaVaultBalanceUpdateEvent = ( diff --git a/src/typescript/sdk/tests/e2e/arena/candlesticks.test.ts b/src/typescript/sdk/tests/e2e/arena/candlesticks.test.ts new file mode 100644 index 000000000..33b6a7cfe --- /dev/null +++ b/src/typescript/sdk/tests/e2e/arena/candlesticks.test.ts @@ -0,0 +1,345 @@ +import { + ArenaPeriod, + calculateCurvePrice, + ONE_APT_BIGINT, + sleep, + type SymbolEmoji, +} from "../../../src"; +import { EmojicoinClient } from "../../../src/client/emojicoin-client"; +import { + type ArenaCandlestickModel, + type MarketLatestStateEventModel, + postgrest, + TableName, + toArenaCandlestickModel, + toMarketLatestStateEventModel, + waitForEmojicoinIndexer, +} from "../../../src/indexer-v2"; +import { + fetchArenaMeleeView, + fetchMeleeEmojiData, + type MeleeEmojiData, +} from "../../../src/markets/arena-utils"; +import { type FundedAccountIndex, getFundedAccount } from "../../utils/test-accounts"; +import { + ONE_SECOND_MICROSECONDS, + setNextMeleeDurationAndEnsureCrank, + waitUntilCurrentMeleeEnds, + PROCESSING_WAIT_TIME, + waitForProcessor, +} from "./utils"; +import { getPublisher } from "../../utils/helpers"; +import type { Account } from "@aptos-labs/ts-sdk"; + +const TWO_SECONDS = 2000; + +describe("ensures arena candlesticks work", () => { + const emojicoin = new EmojicoinClient(); + + let melee: MeleeEmojiData; + + const MELEE_DURATION = ONE_SECOND_MICROSECONDS * 60n; + + const publisher = getPublisher(); + + const emojis: SymbolEmoji[][] = [ + ["♑"], + ["♒"], + ["♈"], + ["♎"], + ["♍"], + ["♊"], + ["♌"], + ["⛎"], + ["♓"], + ["♐"], + ["♏"], + ["♉"], + ]; + + beforeAll(async () => { + for (const emoji of emojis) { + await emojicoin.register(getFundedAccount("667"), emoji); + } + await waitUntilCurrentMeleeEnds(); + await setNextMeleeDurationAndEnsureCrank(MELEE_DURATION).then((res) => { + melee = res.melee; + return waitForEmojicoinIndexer(res.version, PROCESSING_WAIT_TIME); + }); + }, 70000); + + beforeEach(async () => { + await waitUntilCurrentMeleeEnds(); + // Crank the melee to end it and start a new one. + const res = await emojicoin.arena.enter( + publisher, + 1n, + false, + melee.market1.symbolEmojis, + melee.market2.symbolEmojis, + "symbol1" + ); + melee = await fetchArenaMeleeView(res.arena.event.meleeID).then(fetchMeleeEmojiData); + await waitForProcessor(res); + + return true; + }, 70000); + + it("verifies that arena candlesticks are correct on markets without prior data", async () => { + // We have to swap with the accounts that registered the markets as the + // markets were never traded on and can be in the grace period. + // + // The market registrant can either be 0xf00d if the market is one of + // the two initial markets, or one of the funded accounts (0x000 to 0xfff). + + const registrant0: string = await postgrest + .from(TableName.MarketRegistrationEvents) + .select("registrant") + .eq("market_id", melee.market1.marketID) + .then((r) => r.data![0].registrant.substring(2, 5)); + const registrant1: string = await postgrest + .from(TableName.MarketRegistrationEvents) + .select("registrant") + .eq("market_id", melee.market2.marketID) + .then((r) => r.data![0].registrant.substring(2, 5)); + + let account1: Account; + let account2: Account; + + try { + account1 = getFundedAccount(registrant0 as unknown as FundedAccountIndex); + } catch (e) { + account1 = getPublisher(); + } + + try { + account2 = getFundedAccount(registrant1 as unknown as FundedAccountIndex); + } catch (e) { + account2 = getPublisher(); + } + + let candlesticks: ArenaCandlestickModel[] | null = null; + let fifteenSecondCandles: ArenaCandlestickModel[] = []; + let expectedPrice = 1; + let expectedVolume = 0n; + let state0: MarketLatestStateEventModel = {} as MarketLatestStateEventModel; + let state1: MarketLatestStateEventModel = {} as MarketLatestStateEventModel; + + const refreshCandlesticksData = async () => { + candlesticks = await postgrest + .from(TableName.ArenaCandlesticks) + .select("*") + .eq("melee_id", melee.view.meleeID) + .then((r) => r.data) + .then((r) => (r === null ? null : r.map(toArenaCandlestickModel))); + }; + const refreshStateData = async () => { + state0 = await postgrest + .from(TableName.MarketLatestStateEvent) + .select("*") + .eq("market_id", melee.market1.marketID) + .single() + .then((r) => r.data) + .then((r) => toMarketLatestStateEventModel(r)); + state1 = await postgrest + .from(TableName.MarketLatestStateEvent) + .select("*") + .eq("market_id", melee.market2.marketID) + .single() + .then((r) => r.data) + .then((r) => toMarketLatestStateEventModel(r)); + }; + const waitForNew15sPeriodBoundary = async () => { + const now = new Date().getTime(); + const fifteenSecondStart = now - (now % 15000); + const fifteenSecondEnd = fifteenSecondStart + 15000; + const bufferForTimeDrift = TWO_SECONDS; + const timeToWait = Math.max(fifteenSecondEnd - now, 0) + bufferForTimeDrift; + await sleep(timeToWait); + }; + const calculatePrice = ( + state0: MarketLatestStateEventModel, + state1: MarketLatestStateEventModel + ) => { + const price0 = calculateCurvePrice(state0.state); + const price1 = calculateCurvePrice(state1.state); + return price0.div(price1).toNumber(); + }; + + await refreshCandlesticksData(); + await refreshStateData(); + + expect(candlesticks).not.toBeNull(); + expect(candlesticks).toHaveLength(0); + + expect(state0.lastSwap.avgExecutionPriceQ64).toEqual(0n); + expect(state1.lastSwap.avgExecutionPriceQ64).toEqual(0n); + + await waitForNew15sPeriodBoundary(); + + // ATM, no swaps are present on either market. + + await waitForProcessor( + await emojicoin.arena.enter( + account1, + ONE_APT_BIGINT, + false, + melee.market1.symbolEmojis, + melee.market2.symbolEmojis, + "symbol1" + ) + ); + + await refreshCandlesticksData(); + await refreshStateData(); + + expect(candlesticks).not.toBeNull(); + expect(candlesticks!.length).toBeGreaterThan(0); + + fifteenSecondCandles = candlesticks!.filter((c) => c.period === ArenaPeriod.Period15S); + + expectedPrice = calculatePrice(state0, state1); + const expectedOpenPrice = expectedPrice; + + expect(fifteenSecondCandles).toHaveLength(1); + expect(fifteenSecondCandles[0].nSwaps).toEqual(1n); + expect(fifteenSecondCandles[0].lowPrice).toEqual(expectedPrice); + expect(fifteenSecondCandles[0].highPrice).toEqual(expectedPrice); + expect(fifteenSecondCandles[0].openPrice).toEqual(expectedOpenPrice); + expect(fifteenSecondCandles[0].closePrice).toEqual(expectedPrice); + expect(fifteenSecondCandles[0].volume).toEqual(state0!.lastSwap.quoteVolume); + + // No swap is generated from the exit. + + await emojicoin.arena.exit(account1, melee.market1.symbolEmojis, melee.market2.symbolEmojis); + + // Here, we make a swap on the other market. + + await waitForProcessor( + await emojicoin.arena.enter( + account2, + ONE_APT_BIGINT, + false, + melee.market1.symbolEmojis, + melee.market2.symbolEmojis, + "symbol2" + ) + ); + + await refreshCandlesticksData(); + await refreshStateData(); + + let oldExpectedPrice = expectedPrice; + expectedPrice = calculatePrice(state0, state1); + + expectedVolume = state0!.lastSwap.quoteVolume + state1!.lastSwap.quoteVolume; + + expect(candlesticks).not.toBeNull(); + + fifteenSecondCandles = candlesticks!.filter((c) => c.period === ArenaPeriod.Period15S); + + expect(fifteenSecondCandles).toHaveLength(1); + expect(fifteenSecondCandles[0].nSwaps).toEqual(2n); + expect(fifteenSecondCandles[0].lowPrice).toBeCloseTo(expectedPrice, 5); + expect(fifteenSecondCandles[0].highPrice).toBeCloseTo(oldExpectedPrice, 5); + expect(fifteenSecondCandles[0].openPrice).toBeCloseTo(expectedOpenPrice, 5); + expect(fifteenSecondCandles[0].closePrice).toBeCloseTo(expectedPrice, 5); + expect(fifteenSecondCandles[0].volume).toEqual(expectedVolume); + + // We swap for emojicoin 1 to emojicoin 0. + + await waitForProcessor( + await emojicoin.arena.swap(account2, melee.market1.symbolEmojis, melee.market2.symbolEmojis) + ); + + await refreshCandlesticksData(); + await refreshStateData(); + + expect(candlesticks).not.toBeNull(); + + oldExpectedPrice = expectedPrice; + + expectedPrice = calculatePrice(state0!, state1!); + expectedVolume += state0!.lastSwap.quoteVolume + state1!.lastSwap.quoteVolume; + fifteenSecondCandles = candlesticks!.filter((c) => c.period === ArenaPeriod.Period15S); + + expect(fifteenSecondCandles).toHaveLength(1); + expect(fifteenSecondCandles[0].nSwaps).toEqual(4n); + expect(fifteenSecondCandles[0].lowPrice).toBeCloseTo(oldExpectedPrice, 5); + expect(fifteenSecondCandles[0].highPrice).toBeCloseTo(expectedPrice, 5); + expect(fifteenSecondCandles[0].openPrice).toBeCloseTo(expectedOpenPrice, 5); + expect(fifteenSecondCandles[0].closePrice).toBeCloseTo(expectedPrice, 5); + expect(fifteenSecondCandles[0].volume).toEqual(expectedVolume); + + await waitForNew15sPeriodBoundary(); + + // This swap should happen in the next candlestick boundary, so it should generate a new one. + + await waitForProcessor( + await emojicoin.arena.swap(account2, melee.market1.symbolEmojis, melee.market2.symbolEmojis) + ); + + const oldSwap1 = state1!; + + await refreshCandlesticksData(); + await refreshStateData(); + + expect(candlesticks).not.toBeNull(); + + fifteenSecondCandles = candlesticks!.filter((c) => c.period === ArenaPeriod.Period15S); + + expect(fifteenSecondCandles).toHaveLength(2); + // We check that the previous candle hasn't changed + expect(fifteenSecondCandles[0].nSwaps).toEqual(4n); + expect(fifteenSecondCandles[0].lowPrice).toBeCloseTo(oldExpectedPrice, 5); + expect(fifteenSecondCandles[0].highPrice).toBeCloseTo(expectedPrice, 5); + expect(fifteenSecondCandles[0].openPrice).toBeCloseTo(expectedOpenPrice, 5); + expect(fifteenSecondCandles[0].closePrice).toBeCloseTo(expectedPrice, 5); + expect(fifteenSecondCandles[0].volume).toEqual(expectedVolume); + + expectedVolume = state0!.lastSwap.quoteVolume + state1!.lastSwap.quoteVolume; + + // When an arena swap happens, they are actually slightly staggered, meaning + // that two swaps happen (one on each market), one after the other. This + // means that with one arena swap, there are two price updates. + // + // Example: + // + // Emojicoin A and emojicoin B both have the same price: 1. + // + // This makes the price of A/B also 1. + // + // Someone makes an arena swap from A to B. + // + // First, there is a swap (sell) on market A, where emojicoin A is sold for APT. + // + // This makes the price of emojicoin A go down to 0.99. + // + // The A/B price is now 0.99 (0.99 / 1). + // + // Then, there is a swap (buy) on market B, where APT is sold for emojicoin B. + // + // This marks the price of emojicoin B go up to 1.01. + // + // The A/B price is now ~0.98 (0.99 / 1.01). + // + // The same mechanism happens in reverse when there is a swap from B to A. + // + // If the initial price of A/B is 1, the end price would be ~1.02, with an + // intermediary price of 1.01. + // + // Because of this, despite there only being one "arena swap" in this + // candlestick time boundary, there are two different prices for low/high + // and for open/close. + + const intermediaryExpectedPrice = calculatePrice(state0!, oldSwap1); + expectedPrice = calculatePrice(state0!, state1!); + + expect(fifteenSecondCandles[1].lowPrice).toBeCloseTo(expectedPrice, 5); + expect(fifteenSecondCandles[1].highPrice).toBeCloseTo(intermediaryExpectedPrice, 5); + expect(fifteenSecondCandles[1].openPrice).toBeCloseTo(intermediaryExpectedPrice, 5); + expect(fifteenSecondCandles[1].closePrice).toBeCloseTo(expectedPrice, 5); + expect(fifteenSecondCandles[1].nSwaps).toEqual(2n); + expect(fifteenSecondCandles[1].volume).toEqual(expectedVolume); + }, 70000); +}); diff --git a/src/typescript/sdk/tests/e2e/arena/general/arena.test.ts b/src/typescript/sdk/tests/e2e/arena/general.test.ts similarity index 75% rename from src/typescript/sdk/tests/e2e/arena/general/arena.test.ts rename to src/typescript/sdk/tests/e2e/arena/general.test.ts index a8a91b640..c8d0dc95e 100644 --- a/src/typescript/sdk/tests/e2e/arena/general/arena.test.ts +++ b/src/typescript/sdk/tests/e2e/arena/general.test.ts @@ -1,13 +1,13 @@ -import type { Ed25519Account, Account, UserTransactionResponse } from "@aptos-labs/ts-sdk"; +import type { Ed25519Account, Account } from "@aptos-labs/ts-sdk"; import { - type AnyNumberString, ARENA_MODULE_ADDRESS, EmojicoinArena, getAptosClient, ONE_APT_BIGINT, + sleep, type SymbolEmoji, -} from "../../../../src"; -import { EmojicoinClient } from "../../../../src/client/emojicoin-client"; +} from "../../../src"; +import { EmojicoinClient } from "../../../src/client/emojicoin-client"; import { type ArenaEnterModel, type ArenaExitModel, @@ -25,19 +25,22 @@ import { toArenaPositionModel, toArenaSwapModel, waitForEmojicoinIndexer, -} from "../../../../src/indexer-v2"; +} from "../../../src/indexer-v2"; import { fetchArenaMeleeView, + fetchArenaRegistryView, fetchMeleeEmojiData, type MeleeEmojiData, -} from "../../../../src/markets/arena-utils"; -import { getFundedAccount } from "../../../utils/test-accounts"; +} from "../../../src/markets/arena-utils"; +import { type FundedAccountIndex, getFundedAccount } from "../../utils/test-accounts"; import { ONE_SECOND_MICROSECONDS, setNextMeleeDurationAndEnsureCrank, waitUntilCurrentMeleeEnds, -} from "../utils"; -import { getPublisher } from "../../../utils/helpers"; + PROCESSING_WAIT_TIME, + waitForProcessor, +} from "./utils"; +import { getPublisher } from "../../utils/helpers"; const getEmojicoinLockedDiffFromSwapRes = ( swapRes: Awaited>, @@ -62,17 +65,6 @@ const getEmojicoinLockedDiffFromSwapRes = ( return { emojicoin0Locked, emojicoin1Locked }; }; -const PROCESSING_WAIT_TIME = 2 * 1000; -const waitForProcessor = < - T extends { version: AnyNumberString } | { response: UserTransactionResponse }, ->( - res: T -) => - waitForEmojicoinIndexer( - "version" in res ? res.version : res.response.version, - PROCESSING_WAIT_TIME - ); - const objectKeysWithoutEventIndexAndVersion = (v: object) => { const set = new Set(Object.keys(v)); // Ignore `eventIndex`, `version`, and `eventName`. The db models don't have it but the views do. @@ -91,6 +83,9 @@ const expectObjectEqualityExceptEventIndexAndVersion = (a: object, b: object) => expect(stringifyJSONWithBigInts(newA)).toEqual(stringifyJSONWithBigInts(newB)); }; +const getNextAccountHelper = (i: number) => + getFundedAccount(i.toString().padStart(3, "0") as FundedAccountIndex); + /** * Because this test checks the details of the very first arena it must run separately from other * arena tests. @@ -311,19 +306,19 @@ describe("ensures an arena correctly unfolds and the processor data is accurate" expect(position.emojicoin0Balance).toEqual(viewArenaSwapEvent.emojicoin0Proceeds); expect(position.emojicoin1Balance).toEqual(viewArenaSwapEvent.emojicoin1Proceeds); - const exitResponse = await emojicoin.arena.exit( + const exitResponse1 = await emojicoin.arena.exit( account, melee.market1.symbolEmojis, melee.market2.symbolEmojis ); - expect(exitResponse.events.arenaExitEvents).toHaveLength(1); + expect(exitResponse1.events.arenaExitEvents).toHaveLength(1); - const viewExitEvent = exitResponse.events.arenaExitEvents[0]; + let viewExitEvent = exitResponse1.events.arenaExitEvents[0]; - await waitForProcessor(exitResponse); + await waitForProcessor(exitResponse1); - const arenaExits: ArenaExitModel[] | null = await postgrest + let arenaExits: ArenaExitModel[] | null = await postgrest .from(TableName.ArenaExitEvents) .select("*") .eq("melee_id", melee.view.meleeID) @@ -345,10 +340,13 @@ describe("ensures an arena correctly unfolds and the processor data is accurate" expect(arenaInfo).not.toBeNull(); - const dbExitEvent = arenaExits![0]; + let dbExitEvent = arenaExits![0]; position = arenaPositions![0]; - expectObjectEqualityExceptEventIndexAndVersion(dbExitEvent.exit, viewExitEvent); + expectObjectEqualityExceptEventIndexAndVersion(dbExitEvent.exit, { + ...viewExitEvent, + duringMelee: true, + }); expect(position.user).toEqual(viewExitEvent.user); expect(position.meleeID).toEqual(viewExitEvent.meleeID); @@ -365,6 +363,47 @@ describe("ensures an arena correctly unfolds and the processor data is accurate" expect(position.withdrawals).toBeLessThanOrEqual((withdrawalsApt * 10001n) / 10000n); expect(position.emojicoin0Balance).toEqual(0n); expect(position.emojicoin1Balance).toEqual(0n); + + await waitForProcessor( + await emojicoin.arena.enter( + account, + 1n * 10n ** 8n, + false, + melee.market1.symbolEmojis, + melee.market2.symbolEmojis, + "symbol1" + ) + ); + + await waitUntilCurrentMeleeEnds(); + + const exitResponse2 = await emojicoin.arena.exit( + account, + melee.market1.symbolEmojis, + melee.market2.symbolEmojis + ); + await waitForProcessor(exitResponse2); + viewExitEvent = exitResponse2.events.arenaExitEvents[0]; + + arenaExits = await postgrest + .from(TableName.ArenaExitEvents) + .select("*") + .eq("melee_id", melee.view.meleeID) + .then((r) => r.data) + .then((r) => (r === null ? null : r.map(toArenaExitModel))); + + dbExitEvent = arenaExits![1]; + + expect(Number(dbExitEvent.exit.aptProceeds) / 10 ** 8).toBeCloseTo( + Number(viewExitEvent.aptProceeds) / 10 ** 8, + 5 + ); + + // Compare proceeds separately as they can differ from the event and db by 1 due to rounding differences + expectObjectEqualityExceptEventIndexAndVersion( + { ...dbExitEvent.exit, aptProceeds: 0n }, + { ...viewExitEvent, aptProceeds: 0n, duringMelee: false } + ); }, 30000); }); @@ -377,6 +416,10 @@ describe("ensures leaderboard history is working", () => { const publisher = getPublisher(); + let accountIndex = 100; + + const getNextAccount = () => getNextAccountHelper(accountIndex++); + // Utility function to avoid repetitive code. Only the `account` and `escrowCoin` differs. const enterHelper = (account: Account, escrowCoin: "symbol1" | "symbol2") => emojicoin.arena.enter( @@ -414,9 +457,9 @@ describe("ensures leaderboard history is working", () => { }, 10000); it("verifies that the leaderboard data is correct", async () => { - const account1 = getFundedAccount("420"); - const account2 = getFundedAccount("421"); - const account3 = getFundedAccount("422"); + const account1 = getNextAccount(); + const account2 = getNextAccount(); + const account3 = getNextAccount(); await enterHelper(account1, "symbol1"); await enterHelper(account2, "symbol1"); await enterHelper(account3, "symbol1"); @@ -438,9 +481,15 @@ describe("ensures leaderboard history is working", () => { expect(leaderboard).not.toBeNull(); expect(leaderboard).toHaveLength(3); - const user1LeaderboardData = leaderboard!.find((l) => l.user.startsWith("0x420"))!; - const user2LeaderboardData = leaderboard!.find((l) => l.user.startsWith("0x421"))!; - const user3LeaderboardData = leaderboard!.find((l) => l.user.startsWith("0x422"))!; + const user1LeaderboardData = leaderboard!.find( + (l) => l.user === account1.accountAddress.toString() + )!; + const user2LeaderboardData = leaderboard!.find( + (l) => l.user === account2.accountAddress.toString() + )!; + const user3LeaderboardData = leaderboard!.find( + (l) => l.user === account3.accountAddress.toString() + )!; expect(user1LeaderboardData).toBeDefined(); expect(user2LeaderboardData).toBeDefined(); @@ -465,7 +514,7 @@ describe("ensures leaderboard history is working", () => { it("verifies the data during a melee with no activity", async () => { await waitUntilCurrentMeleeEnds(); const res = await emojicoin.arena.enter( - getFundedAccount("667"), + getNextAccount(), 1n * 10n ** 8n, false, melee.market1.symbolEmojis, @@ -485,9 +534,9 @@ describe("ensures leaderboard history is working", () => { }, 15000); it("verifies the data during a melee with no swaps", async () => { - const account1 = getFundedAccount("420"); - const account2 = getFundedAccount("421"); - const account3 = getFundedAccount("422"); + const account1 = getNextAccount(); + const account2 = getNextAccount(); + const account3 = getNextAccount(); await enterHelper(account1, "symbol1"); await enterHelper(account2, "symbol2"); await enterHelper(account3, "symbol1"); @@ -508,9 +557,15 @@ describe("ensures leaderboard history is working", () => { expect(leaderboard).not.toBeNull(); expect(leaderboard).toHaveLength(3); - const user1LeaderboardData = leaderboard!.find((l) => l.user.startsWith("0x420"))!; - const user2LeaderboardData = leaderboard!.find((l) => l.user.startsWith("0x421"))!; - const user3LeaderboardData = leaderboard!.find((l) => l.user.startsWith("0x422"))!; + const user1LeaderboardData = leaderboard!.find( + (l) => l.user === account1.accountAddress.toString() + )!; + const user2LeaderboardData = leaderboard!.find( + (l) => l.user === account2.accountAddress.toString() + )!; + const user3LeaderboardData = leaderboard!.find( + (l) => l.user === account3.accountAddress.toString() + )!; expect(user1LeaderboardData).toBeDefined(); expect(user2LeaderboardData).toBeDefined(); @@ -527,9 +582,9 @@ describe("ensures leaderboard history is working", () => { }, 15000); it("verifies the data during a melee with no exits", async () => { - const account1 = getFundedAccount("420"); - const account2 = getFundedAccount("421"); - const account3 = getFundedAccount("422"); + const account1 = getNextAccount(); + const account2 = getNextAccount(); + const account3 = getNextAccount(); await enterHelper(account1, "symbol1"); await enterHelper(account2, "symbol2"); await enterHelper(account3, "symbol1"); @@ -550,9 +605,15 @@ describe("ensures leaderboard history is working", () => { expect(leaderboard).not.toBeNull(); expect(leaderboard).toHaveLength(3); - const user1LeaderboardData = leaderboard!.find((l) => l.user.startsWith("0x420"))!; - const user2LeaderboardData = leaderboard!.find((l) => l.user.startsWith("0x421"))!; - const user3LeaderboardData = leaderboard!.find((l) => l.user.startsWith("0x422"))!; + const user1LeaderboardData = leaderboard!.find( + (l) => l.user === account1.accountAddress.toString() + )!; + const user2LeaderboardData = leaderboard!.find( + (l) => l.user === account2.accountAddress.toString() + )!; + const user3LeaderboardData = leaderboard!.find( + (l) => l.user === account3.accountAddress.toString() + )!; expect(user1LeaderboardData).toBeDefined(); expect(user2LeaderboardData).toBeDefined(); @@ -642,6 +703,10 @@ describe("ensures arena info is working", () => { const publisher = getPublisher(); + let accountIndex = 200; + + const getNextAccount = () => getNextAccountHelper(accountIndex++); + // Utility function to avoid repetitive code. Only the `account` and `escrowCoin` differs. const enterHelper = (account: Account, escrowCoin: "symbol1" | "symbol2") => emojicoin.arena.enter( @@ -679,9 +744,9 @@ describe("ensures arena info is working", () => { }, 30000); it("verifies that all fields in the arena_info table are correctly calculated in a simple trading scenario", async () => { - const account1 = getFundedAccount("420"); - const account2 = getFundedAccount("421"); - const account3 = getFundedAccount("422"); + const account1 = getFundedAccount("423"); + const account2 = getFundedAccount("424"); + const account3 = getFundedAccount("425"); let volume = 0n; @@ -728,14 +793,14 @@ describe("ensures arena info is working", () => { let emojicoin1Locked = 0n; const accounts = [ - getFundedAccount("420"), - getFundedAccount("421"), - getFundedAccount("422"), - getFundedAccount("423"), - getFundedAccount("424"), - getFundedAccount("425"), - getFundedAccount("426"), - getFundedAccount("427"), + getNextAccount(), + getNextAccount(), + getNextAccount(), + getNextAccount(), + getNextAccount(), + getNextAccount(), + getNextAccount(), + getNextAccount(), ]; // Enter with all accounts alternating between symbol 1 and 2. @@ -812,3 +877,153 @@ describe("ensures arena info is working", () => { expect(arenaInfo!.emojicoin1Locked).toEqual(emojicoin1Locked); }, 30000); }); + +describe("ensures arena works in edge cases", () => { + const emojicoin = new EmojicoinClient(); + + let melee: MeleeEmojiData; + + const MELEE_DURATION = ONE_SECOND_MICROSECONDS * 15n; + + const publisher = getPublisher(); + + let accountIndex = 200; + + const getNextAccount = () => getNextAccountHelper(accountIndex++); + + // Utility function to avoid repetitive code. Only the `account` and `escrowCoin` differs. + const enterHelper = (account: Account, escrowCoin: "symbol1" | "symbol2") => + emojicoin.arena.enter( + account, + ONE_APT_BIGINT, + false, + melee.market1.symbolEmojis, + melee.market2.symbolEmojis, + escrowCoin + ); + + beforeAll(async () => { + await waitUntilCurrentMeleeEnds(); + await setNextMeleeDurationAndEnsureCrank(MELEE_DURATION).then((res) => { + melee = res.melee; + return waitForEmojicoinIndexer(res.version, PROCESSING_WAIT_TIME); + }); + }, 30000); + + beforeEach(async () => { + await waitUntilCurrentMeleeEnds(); + // Crank the melee to end it and start a new one. + const res = await emojicoin.arena.enter( + publisher, + 1n, + false, + melee.market1.symbolEmojis, + melee.market2.symbolEmojis, + "symbol1" + ); + melee = await fetchArenaMeleeView(res.arena.event.meleeID).then(fetchMeleeEmojiData); + await waitForProcessor(res); + + return true; + }, 30000); + + it("verifies that a swap after a melee has ended and has been cranked is indexed properly", async () => { + const account1 = getNextAccount(); + const account2 = getNextAccount(); + + await enterHelper(account1, "symbol1"); + await enterHelper(account2, "symbol1"); + + await emojicoin.arena.exit(account1, melee.market1.symbolEmojis, melee.market2.symbolEmojis); + + await waitUntilCurrentMeleeEnds(); + + // In order to crank + await enterHelper(account1, "symbol1"); + + const registry = await fetchArenaRegistryView(); + + expect(registry.currentMeleeID).toEqual(melee.view.meleeID + 1n); + + await sleep(2000); + + await waitForProcessor( + await emojicoin.arena.swap(account2, melee.market1.symbolEmojis, melee.market2.symbolEmojis) + ); + + const swaps = await postgrest + .from(TableName.ArenaSwapEvents) + .select("*") + .eq("melee_id", melee.view.meleeID) + .order("transaction_version") + .then((r) => r.data) + .then((r) => (r === null ? null : r.map(toArenaSwapModel))); + + const exits = await postgrest + .from(TableName.ArenaExitEvents) + .select("*") + .eq("melee_id", melee.view.meleeID) + .order("transaction_version") + .then((r) => r.data) + .then((r) => (r === null ? null : r.map(toArenaExitModel))); + + const positions = await postgrest + .from(TableName.ArenaPosition) + .select("*") + .eq("melee_id", melee.view.meleeID) + .then((r) => r.data) + .then((r) => (r === null ? null : r.map(toArenaPositionModel))); + + const leaderboard = await postgrest + .from(TableName.ArenaLeaderboardHistory) + .select("*") + .eq("melee_id", melee.view.meleeID) + .then((r) => r.data) + .then((r) => (r === null ? null : r.map(toArenaLeaderboardHistoryModel))); + + expect(swaps).not.toBeNull(); + expect(swaps).toHaveLength(1); + + expect(swaps![0].swap.duringMelee).toEqual(false); + + expect(exits).not.toBeNull(); + expect(exits).toHaveLength(2); + + expect(exits![0].exit.duringMelee).toEqual(true); + expect(exits![1].exit.duringMelee).toEqual(false); + + expect(positions).not.toBeNull(); + expect(positions).toHaveLength(2); + expect(positions).toHaveLength(2); + + const position1 = positions!.find((p) => p.user === account1.accountAddress.toString())!; + const position2 = positions!.find((p) => p.user === account2.accountAddress.toString())!; + + expect(position1.open).toEqual(false); + expect(position2.open).toEqual(false); + + expect(position1.lastExit0).toEqual(true); + expect(position2.lastExit0).toEqual(false); + + expect(leaderboard).not.toBeNull(); + expect(leaderboard).toHaveLength(2); + + const leaderboard1 = leaderboard!.find((l) => l.user === account1.accountAddress.toString())!; + const leaderboard2 = leaderboard!.find((l) => l.user === account2.accountAddress.toString())!; + + expect(leaderboard1.exited).toEqual(true); + expect(leaderboard2.exited).toEqual(true); + + expect(leaderboard1.emojicoin0Balance).toEqual(0n); + expect(leaderboard2.emojicoin0Balance).toBeGreaterThan(0n); + + expect(leaderboard1.emojicoin1Balance).toEqual(0n); + expect(leaderboard2.emojicoin1Balance).toEqual(0n); + + expect(leaderboard1.withdrawals).toBeGreaterThan(0n); + expect(leaderboard2.withdrawals).toEqual(0n); + + expect(leaderboard1.lastExit0).toEqual(true); + expect(leaderboard2.lastExit0).toEqual(false); + }, 30000); +}); diff --git a/src/typescript/sdk/tests/e2e/arena/utils.ts b/src/typescript/sdk/tests/e2e/arena/utils.ts index 752415027..235f59fc0 100644 --- a/src/typescript/sdk/tests/e2e/arena/utils.ts +++ b/src/typescript/sdk/tests/e2e/arena/utils.ts @@ -1,15 +1,17 @@ // cspell:word funder import { EmojicoinArena } from "@/contract-apis"; -import { type Account } from "@aptos-labs/ts-sdk"; +import type { UserTransactionResponse, Account } from "@aptos-labs/ts-sdk"; import { type SymbolEmoji, fetchArenaRegistryView, fetchArenaMeleeView, fetchMeleeEmojiData, + type AnyNumberString, } from "../../../src"; import { EmojicoinClient } from "../../../src/client/emojicoin-client"; import { getPublisher, getAptosClient } from "../../utils"; +import { waitForEmojicoinIndexer } from "../../../src/indexer-v2"; /** * Have the publisher register a third market and trade on all three markets to unlock them for @@ -112,3 +114,15 @@ export const depositToVault = async (funder: Account, amount: bigint) => funder, amount, }); + +export const PROCESSING_WAIT_TIME = 2 * 1000; + +export const waitForProcessor = < + T extends { version: AnyNumberString } | { response: UserTransactionResponse }, +>( + res: T +) => + waitForEmojicoinIndexer( + "version" in res ? res.version : res.response.version, + PROCESSING_WAIT_TIME + ); diff --git a/src/typescript/sdk/tests/e2e/arena/ws/arena-websockets.test.ts b/src/typescript/sdk/tests/e2e/arena/websockets.test.ts similarity index 94% rename from src/typescript/sdk/tests/e2e/arena/ws/arena-websockets.test.ts rename to src/typescript/sdk/tests/e2e/arena/websockets.test.ts index 3d12cbfe5..c147b9ce1 100644 --- a/src/typescript/sdk/tests/e2e/arena/ws/arena-websockets.test.ts +++ b/src/typescript/sdk/tests/e2e/arena/websockets.test.ts @@ -1,23 +1,23 @@ // cspell:word funder -import { getEvents, ONE_APT_BIGINT, type SymbolEmoji } from "../../../../src"; -import { EmojicoinClient } from "../../../../src/client/emojicoin-client"; -import { type MeleeEmojiData } from "../../../../src/markets/arena-utils"; +import { getEvents, ONE_APT_BIGINT, type SymbolEmoji } from "../../../src"; +import { EmojicoinClient } from "../../../src/client/emojicoin-client"; +import { type MeleeEmojiData } from "../../../src/markets/arena-utils"; import { isArenaEnterModel, isArenaExitModel, isArenaSwapModel, isArenaVaultBalanceUpdateModel, -} from "../../../../src/types/arena-types"; -import { getFundedAccount } from "../../../utils/test-accounts"; -import { compareParsedData, connectNewClient, customWaitFor, subscribe } from "../../broker/utils"; +} from "../../../src/types/arena-types"; +import { getFundedAccount } from "../../utils/test-accounts"; +import { compareParsedData, connectNewClient, customWaitFor, subscribe } from "../broker/utils"; import { registerAndUnlockInitialMarketsForArenaTest, setNextMeleeDurationAndEnsureCrank, ONE_SECOND_MICROSECONDS, depositToVault, waitUntilCurrentMeleeEnds, -} from "../utils"; +} from "./utils"; describe("tests to ensure that arena websocket events work as expected", () => { const user = getFundedAccount("085"); diff --git a/src/typescript/sdk/tests/e2e/publish.test.ts b/src/typescript/sdk/tests/e2e/publish.test.ts index 7d2df7b93..9223130a4 100644 --- a/src/typescript/sdk/tests/e2e/publish.test.ts +++ b/src/typescript/sdk/tests/e2e/publish.test.ts @@ -26,8 +26,8 @@ describe("tests publishing modules to a local network", () => { packageDirRelativeToRoot: `src/move/${packageName}`, }); - expect(AccountAddress.from(publishResult.sender).toStringLong()).toEqual( - publisher.accountAddress.toStringLong() + expect(AccountAddress.from(publishResult.sender).toString()).toEqual( + publisher.accountAddress.toString() ); expect(publishResult.success).toEqual(true); diff --git a/src/typescript/sdk/tests/unit/address-derivation.test.ts b/src/typescript/sdk/tests/unit/address-derivation.test.ts index bf5dfd09a..4c7998aa6 100644 --- a/src/typescript/sdk/tests/unit/address-derivation.test.ts +++ b/src/typescript/sdk/tests/unit/address-derivation.test.ts @@ -12,8 +12,6 @@ describe("it derives emojicoin addresses", () => { const emojis: Array = ["🦓", "🧟"]; const derivedNamedObjectFromRawEmojis = getMarketAddress(emojis, registryAddress); - expect(derivedNamedObjectFromRawEmojis.toStringLong()).toEqual( - expectedObjectAddress.toStringLong() - ); + expect(derivedNamedObjectFromRawEmojis.toString()).toEqual(expectedObjectAddress.toString()); }); }); diff --git a/src/typescript/turbo.json b/src/typescript/turbo.json index 0726aa4ce..5b0796dc1 100644 --- a/src/typescript/turbo.json +++ b/src/typescript/turbo.json @@ -80,6 +80,10 @@ "cache": false, "outputs": [] }, + "test:sdk:arena-candlesticks": { + "cache": false, + "outputs": [] + }, "test:sdk:parallel": { "cache": false, "outputs": []