diff --git a/src/typescript/frontend/src/components/charts/PrivateChart.tsx b/src/typescript/frontend/src/components/charts/PrivateChart.tsx index 56be07808..e6bb657f2 100644 --- a/src/typescript/frontend/src/components/charts/PrivateChart.tsx +++ b/src/typescript/frontend/src/components/charts/PrivateChart.tsx @@ -29,7 +29,7 @@ import { emojisToName } from "lib/utils/emojis-to-name-or-symbol"; import { useEventStore } from "context/event-store-context"; import { getPeriodStartTimeFromTime } from "@sdk/utils"; import { getAptosConfig } from "lib/utils/aptos-client"; -import { getEmojisInString, symbolToEmojis, toMarketEmojiData } from "@sdk/emoji_data"; +import { getSymbolEmojisInString, symbolToEmojis, toMarketEmojiData } from "@sdk/emoji_data"; import { type MarketMetadataModel } from "@sdk/indexer-v2/types"; import { getMarketResource } from "@sdk/markets"; import { Aptos } from "@aptos-labs/ts-sdk"; @@ -270,7 +270,7 @@ export const Chart = async (props: ChartContainerProps) => { throw new Error(`No ticker for symbol: ${symbolInfo}`); } const period = ResolutionStringToPeriod[resolution.toString()]; - const marketEmojis = getEmojisInString(symbolInfo.ticker); + const marketEmojis = getSymbolEmojisInString(symbolInfo.ticker); subscribeToPeriod({ marketEmojis, period, @@ -282,7 +282,7 @@ export const Chart = async (props: ChartContainerProps) => { // For example: `🚀_#_5` for the `🚀` market for a resolution of period `5`. const [symbol, resolution] = subscriberUID.split("_#_"); const period = ResolutionStringToPeriod[resolution]; - const marketEmojis = getEmojisInString(symbol); + const marketEmojis = getSymbolEmojisInString(symbol); unsubscribeFromPeriod({ marketEmojis, period, diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/chat/ChatBox.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/chat/ChatBox.tsx index 42420483c..de32f79ba 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/chat/ChatBox.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/chat/ChatBox.tsx @@ -48,7 +48,7 @@ const ChatBox = (props: ChatProps) => { user: account.address, marketAddress, emojiBytes, - emojiIndicesSequence: new Uint8Array(emojiIndicesSequence), + emojiIndicesSequence, typeTags: [emojicoin, emojicoinLP], }); const res = await submit(builderLambda); diff --git a/src/typescript/package.json b/src/typescript/package.json index a29b62a89..105641293 100644 --- a/src/typescript/package.json +++ b/src/typescript/package.json @@ -19,6 +19,7 @@ "scripts": { "build": "pnpm i && pnpm load-env -- turbo run build", "build:debug": "pnpm i && pnpm load-env -- turbo run build:debug", + "check": "turbo run check", "clean": "turbo run clean --no-cache --force && rm -rf .turbo && rm -rf sdk/.turbo && rm -rf frontend/.turbo && rm -rf frontend/.next", "dev": "pnpm load-env -- turbo run dev --force --parallel --continue", "dev:debug": "pnpm dotenv -v FETCH_DEBUG=true -- pnpm run dev", diff --git a/src/typescript/sdk/package.json b/src/typescript/sdk/package.json index ab43db019..1fc23d47a 100644 --- a/src/typescript/sdk/package.json +++ b/src/typescript/sdk/package.json @@ -56,7 +56,7 @@ "_format": "prettier 'src/**/*.ts' 'tests/**/*.ts' '.eslintrc.js'", "build": "tsc", "build:debug": "BUILD_DEBUG=true pnpm run build", - "check": "tsc --noEmit", + "check": "tsc -p tests/tsconfig.json --noEmit", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", "e2e:testnet": "pnpm load-test-env -v NO_TEST_SETUP=true -- pnpm jest tests/e2e/queries/testnet", "format": "pnpm _format --write", diff --git a/src/typescript/sdk/src/client/emojicoin-client.ts b/src/typescript/sdk/src/client/emojicoin-client.ts new file mode 100644 index 000000000..44f7d1d69 --- /dev/null +++ b/src/typescript/sdk/src/client/emojicoin-client.ts @@ -0,0 +1,369 @@ +import { + AccountAddress, + type Account, + type UserTransactionResponse, + type AccountAddressInput, + type Aptos, + type TypeTag, + type InputGenerateTransactionOptions, + type WaitForTransactionOptions, +} from "@aptos-labs/ts-sdk"; +import { type ChatEmoji, type SymbolEmoji } from "../emoji_data/types"; +import { getEvents } from "../emojicoin_dot_fun"; +import { + Chat, + ProvideLiquidity, + RemoveLiquidity, + RegisterMarket, + Swap, + SwapWithRewards, +} from "../emojicoin_dot_fun/emojicoin-dot-fun"; +import { type Events } from "../emojicoin_dot_fun/events"; +import { getEmojicoinMarketAddressAndTypeTags } from "../markets"; +import { type EventsModels, getEventsAsProcessorModelsFromResponse } from "../mini-processor"; +import { getAptosClient } from "../utils/aptos-client"; +import { toChatMessageEntryFunctionArgs } from "../emoji_data"; +import customExpect from "./expect"; +import { INTEGRATOR_ADDRESS } from "../const"; + +const { expect, Expect } = customExpect; + +type Options = { + feePayer?: Account; + options?: InputGenerateTransactionOptions; + waitForTransactionOptions?: WaitForTransactionOptions; +}; + +/** + * A helper class intended to streamline the process of submitting transactions and using utility + * functions for emojis and market symbols. + * + * The class is created with an optional `Aptos` client, defaulting to creating an `Aptos` client + * from the current network in the environment variables. + * + * Each transaction submission automatically parses the event data and sorts it into corresponding + * events and models, offering an easy way to extract the most relevant event data- i.e., the + * RegisterMarket event for `register`ing a market, or the `Swap` event for `swap`ping. + * + * The `swap` function is separated into `buy` and `sell` to reduce the amount of input arguments. + * + * The `provide_liquidity` and `remove_liquidity` functions in the contract are both under + * `liquidity` as `provide` and `remove`, respectively. + * + * The `utils` functions provides several commonly used utility functions. + * + * @example + * ```typescript + * const emojis: MarketSymbolEmojis = ["🌊"]; + * const emojicoin = new EmojicoinClient(); + * const account = Account.generate(); + * const integrator = account.accountAddress; + * await emojicoin.register(account, emojis, { integrator }); + * const buyArgs = { + * inputAmount: 100n, + * minOutputAmount: 1n, + * integrator, + * integratorFeeRateBPs: 0, + * }; + * await emojicoin.buy(account, emojis, buyArgs); + * ``` + */ +export class EmojicoinClient { + public aptos: Aptos; + + public liquidity = { + provide: this.provideLiquidity.bind(this), + remove: this.removeLiquidity.bind(this), + }; + + public utils = { + emojisToHexStrings: this.emojisToHexStrings.bind(this), + emojisToHexSymbol: this.emojisToHexSymbol.bind(this), + getEmojicoinInfo: this.getEmojicoinInfo.bind(this), + getTransactionEventData: this.getTransactionEventData.bind(this), + }; + + public rewards = { + buy: this.buyWithRewards.bind(this), + sell: this.sellWithRewards.bind(this), + }; + + private integrator: AccountAddress; + + private integratorFeeRateBPs: number; + + private minOutputAmount: bigint; + + constructor(args?: { + aptos?: Aptos; + integrator?: AccountAddressInput; + integratorFeeRateBPs?: bigint | number; + minOutputAmount?: bigint | number; + }) { + const { + aptos = getAptosClient().aptos, + integrator = INTEGRATOR_ADDRESS, + integratorFeeRateBPs = 0, + minOutputAmount = 1n, + } = args ?? {}; + this.aptos = aptos; + this.integrator = AccountAddress.from(integrator); + this.integratorFeeRateBPs = Number(integratorFeeRateBPs); + this.minOutputAmount = BigInt(minOutputAmount); + } + + async register(registrant: Account, symbolEmojis: SymbolEmoji[], options?: Options) { + const response = await RegisterMarket.submit({ + aptosConfig: this.aptos.config, + registrant, + emojis: this.emojisToHexStrings(symbolEmojis), + integrator: this.integrator, + ...options, + }); + const res = this.getTransactionEventData(response); + return { + ...res, + registration: { + event: expect(res.events.marketRegistrationEvents.at(0), Expect.Register.Event), + model: expect(res.models.marketRegistrationEvents.at(0), Expect.Register.Model), + }, + }; + } + + async chat( + user: Account, + symbolEmojis: SymbolEmoji[], + message: string | (SymbolEmoji | ChatEmoji)[], + options?: Options + ) { + const { emojiBytes, emojiIndicesSequence } = + typeof message === "string" + ? toChatMessageEntryFunctionArgs(message) + : toChatMessageEntryFunctionArgs(message.join("")); + + const response = await Chat.submit({ + aptosConfig: this.aptos.config, + user, + emojiBytes, + emojiIndicesSequence, + ...options, + ...this.getEmojicoinInfo(symbolEmojis), + }); + const res = this.getTransactionEventData(response); + return { + ...res, + chat: { + event: expect(res.events.chatEvents.at(0), Expect.Chat.Event), + model: expect(res.models.chatEvents.at(0), Expect.Chat.Model), + }, + }; + } + + async buy( + swapper: Account, + symbolEmojis: SymbolEmoji[], + inputAmount: bigint | number, + options?: Options + ) { + return await this.swap( + swapper, + symbolEmojis, + { + inputAmount, + integrator: this.integrator, + integratorFeeRateBPs: this.integratorFeeRateBPs, + minOutputAmount: this.minOutputAmount, + isSell: false, + }, + options + ); + } + + async sell( + swapper: Account, + symbolEmojis: SymbolEmoji[], + inputAmount: bigint | number, + options?: Options + ) { + return await this.swap( + swapper, + symbolEmojis, + { + inputAmount, + integrator: this.integrator, + integratorFeeRateBPs: this.integratorFeeRateBPs, + minOutputAmount: this.minOutputAmount, + isSell: true, + }, + options + ); + } + + private async swap( + swapper: Account, + symbolEmojis: SymbolEmoji[], + args: { + isSell: boolean; + inputAmount: bigint | number; + minOutputAmount: bigint | number; + integrator: AccountAddressInput; + integratorFeeRateBPs: number; + }, + options?: Options + ) { + const response = await Swap.submit({ + aptosConfig: this.aptos.config, + swapper, + ...args, + ...options, + ...this.getEmojicoinInfo(symbolEmojis), + }); + const res = this.getTransactionEventData(response); + return { + ...res, + swap: { + event: expect(res.events.swapEvents.at(0), Expect.Swap.Event), + model: expect(res.models.swapEvents.at(0), Expect.Swap.Model), + }, + }; + } + + private async buyWithRewards( + swapper: Account, + symbolEmojis: SymbolEmoji[], + inputAmount: bigint | number, + options?: Options + ) { + return await this.swapWithRewards( + swapper, + symbolEmojis, + { inputAmount, minOutputAmount: this.minOutputAmount, isSell: false }, + options + ); + } + + private async sellWithRewards( + swapper: Account, + symbolEmojis: SymbolEmoji[], + inputAmount: bigint | number, + options?: Options + ) { + return await this.swapWithRewards( + swapper, + symbolEmojis, + { inputAmount, minOutputAmount: this.minOutputAmount, isSell: true }, + options + ); + } + + private async swapWithRewards( + swapper: Account, + symbolEmojis: SymbolEmoji[], + args: { + isSell: boolean; + inputAmount: bigint | number; + minOutputAmount: bigint | number; + }, + options?: Options + ) { + const response = await SwapWithRewards.submit({ + aptosConfig: this.aptos.config, + swapper, + ...args, + ...options, + ...this.getEmojicoinInfo(symbolEmojis), + }); + const res = this.getTransactionEventData(response); + return { + ...res, + swap: { + event: expect(res.events.swapEvents.at(0), Expect.Swap.Event), + model: expect(res.models.swapEvents.at(0), Expect.Swap.Model), + }, + }; + } + + private async provideLiquidity( + provider: Account, + symbolEmojis: SymbolEmoji[], + quoteAmount: bigint | number, + options?: Options + ) { + const response = await ProvideLiquidity.submit({ + aptosConfig: this.aptos.config, + provider, + quoteAmount, + minLpCoinsOut: this.minOutputAmount, + ...options, + ...this.getEmojicoinInfo(symbolEmojis), + }); + const res = this.getTransactionEventData(response); + return { + ...res, + liquidity: { + event: expect(res.events.liquidityEvents.at(0), Expect.Liquidity.Event), + model: expect(res.models.liquidityEvents.at(0), Expect.Liquidity.Model), + }, + }; + } + + private async removeLiquidity( + provider: Account, + symbolEmojis: SymbolEmoji[], + lpCoinAmount: bigint | number, + options?: Options + ) { + const response = await RemoveLiquidity.submit({ + aptosConfig: this.aptos.config, + provider, + lpCoinAmount, + minQuoteOut: this.minOutputAmount, + ...options, + ...this.getEmojicoinInfo(symbolEmojis), + }); + const res = this.getTransactionEventData(response); + return { + ...res, + liquidity: { + event: expect(res.events.liquidityEvents.at(0), Expect.Liquidity.Event), + model: expect(res.models.liquidityEvents.at(0), Expect.Liquidity.Model), + }, + }; + } + + private emojisToHexStrings(symbolEmojis: SymbolEmoji[]) { + return symbolEmojis.map((emoji) => new TextEncoder().encode(emoji)); + } + + private emojisToHexSymbol(symbolEmojis: SymbolEmoji[]) { + const res = symbolEmojis.flatMap((emoji) => Array.from(new TextEncoder().encode(emoji))); + return new Uint8Array(res); + } + + private getEmojicoinInfo(symbolEmojis: SymbolEmoji[]): { + marketAddress: AccountAddress; + typeTags: [TypeTag, TypeTag]; + } { + const { marketAddress, emojicoin, emojicoinLP } = getEmojicoinMarketAddressAndTypeTags({ + symbolBytes: this.emojisToHexSymbol(symbolEmojis), + }); + return { + marketAddress, + typeTags: [emojicoin, emojicoinLP], + }; + } + + private getTransactionEventData(response: UserTransactionResponse): { + response: UserTransactionResponse; + events: Events; + models: EventsModels; + } { + const events = getEvents(response); + const models = getEventsAsProcessorModelsFromResponse(response, events); + return { + response, + events, + models, + }; + } +} diff --git a/src/typescript/sdk/src/client/expect.ts b/src/typescript/sdk/src/client/expect.ts new file mode 100644 index 000000000..c03868e07 --- /dev/null +++ b/src/typescript/sdk/src/client/expect.ts @@ -0,0 +1,35 @@ +const expect = (v: T | undefined, message?: string): T => { + if (typeof v === "undefined") { + throw new Error(message ?? "Expected to receive a non-undefined value."); + } + return v; +}; + +const expectErrorMessage = (type: "event" | "model", eventType: string) => + `Expected to receive ${type === "event" ? "an event" : "a model"} of type ${eventType}`; + +const Expect = { + Register: { + Event: expectErrorMessage("event", "MarketRegistrationEvent"), + Model: expectErrorMessage("model", "MarketRegistrationEvent"), + }, + Liquidity: { + Event: expectErrorMessage("event", "LiquidityEvent"), + Model: expectErrorMessage("model", "LiquidityEvent"), + }, + Swap: { + Event: expectErrorMessage("event", "SwapEvent"), + Model: expectErrorMessage("model", "SwapEvent"), + }, + Chat: { + Event: expectErrorMessage("event", "ChatEvent"), + Model: expectErrorMessage("model", "ChatEvent"), + }, +}; + +const customExpect = { + expect, + Expect, +}; + +export default customExpect; diff --git a/src/typescript/sdk/src/const.ts b/src/typescript/sdk/src/const.ts index a61d94461..f814dccfc 100644 --- a/src/typescript/sdk/src/const.ts +++ b/src/typescript/sdk/src/const.ts @@ -4,34 +4,34 @@ import { type ValueOf } from "./utils/utility-types"; import { type DatabaseStructType } from "./indexer-v2/types/json-types"; export const VERCEL = process.env.VERCEL === "1"; -if (!process.env.NEXT_PUBLIC_MODULE_ADDRESS || !process.env.NEXT_PUBLIC_REWARDS_MODULE_ADDRESS) { - let missing = ""; - let missingBoth = false; - if (!process.env.NEXT_PUBLIC_MODULE_ADDRESS) { - missing += "NEXT_PUBLIC_MODULE_ADDRESS"; - } - if (!process.env.NEXT_PUBLIC_REWARDS_MODULE_ADDRESS) { - if (!process.env.NEXT_PUBLIC_MODULE_ADDRESS) { - missingBoth = true; - missing += " and NEXT_PUBLIC_REWARDS_MODULE_ADDRESS"; - } else { - missing += "NEXT_PUBLIC_REWARDS_MODULE_ADDRESS"; - } - } - missing = missing.trimEnd(); - const missingMessage = `Missing ${missing} environment variable${missingBoth ? "s" : ""}`; - let fullErrorMessage = `\n\n${"-".repeat(61)}\n\n${missingMessage}\n`; - if (!VERCEL) { - fullErrorMessage += "Please run this project from the top-level, parent directory.\n"; - } - fullErrorMessage += `\n${"-".repeat(61)}\n`; - throw new Error(fullErrorMessage); +if ( + !process.env.NEXT_PUBLIC_MODULE_ADDRESS || + !process.env.NEXT_PUBLIC_REWARDS_MODULE_ADDRESS || + !process.env.NEXT_PUBLIC_INTEGRATOR_ADDRESS || + !process.env.NEXT_PUBLIC_INTEGRATOR_FEE_RATE_BPS +) { + const missing = [ + ["NEXT_PUBLIC_MODULE_ADDRESS", process.env.NEXT_PUBLIC_MODULE_ADDRESS], + ["NEXT_PUBLIC_REWARDS_MODULE_ADDRESS", process.env.NEXT_PUBLIC_REWARDS_MODULE_ADDRESS], + ["NEXT_PUBLIC_INTEGRATOR_ADDRESS", process.env.NEXT_PUBLIC_INTEGRATOR_ADDRESS], + ["NEXT_PUBLIC_INTEGRATOR_FEE_RATE_BPS", process.env.NEXT_PUBLIC_INTEGRATOR_FEE_RATE_BPS], + ].filter(([_, value]) => !value); + missing.forEach(([key, _]) => { + console.error(`Missing environment variables ${key}`); + }); + throw new Error( + VERCEL + ? `Please set the missing environment variables. ${missing.map(([key, _]) => key).join(", ")}` + : "Please run this project from the top-level, parent directory.\n" + ); } export const MODULE_ADDRESS = (() => AccountAddress.from(process.env.NEXT_PUBLIC_MODULE_ADDRESS))(); export const REWARDS_MODULE_ADDRESS = (() => AccountAddress.from(process.env.NEXT_PUBLIC_REWARDS_MODULE_ADDRESS))(); - +export const INTEGRATOR_ADDRESS = (() => + AccountAddress.from(process.env.NEXT_PUBLIC_INTEGRATOR_ADDRESS))(); +export const INTEGRATOR_FEE_RATE_BPS = Number(process.env.NEXT_PUBLIC_INTEGRATOR_FEE_RATE_BPS); export const ONE_APT = 1 * 10 ** 8; export const ONE_APT_BIGINT = BigInt(ONE_APT); export const APTOS_COIN_TYPE_TAG = parseTypeTag(APTOS_COIN); diff --git a/src/typescript/sdk/src/emoji_data/chat-message.ts b/src/typescript/sdk/src/emoji_data/chat-message.ts index c36f5a3d4..633f3448b 100644 --- a/src/typescript/sdk/src/emoji_data/chat-message.ts +++ b/src/typescript/sdk/src/emoji_data/chat-message.ts @@ -29,6 +29,6 @@ export const toChatMessageEntryFunctionArgs = (message: string) => { } return { emojiBytes: bytesArray, - emojiIndicesSequence: sequence, + emojiIndicesSequence: new Uint8Array(sequence), }; }; diff --git a/src/typescript/sdk/src/emoji_data/utils.ts b/src/typescript/sdk/src/emoji_data/utils.ts index 85e476a2e..1a14b2300 100644 --- a/src/typescript/sdk/src/emoji_data/utils.ts +++ b/src/typescript/sdk/src/emoji_data/utils.ts @@ -10,10 +10,14 @@ import { } from "./types"; import { MAX_SYMBOL_LENGTH } from "../const"; -export const getEmojisInString = (symbols: string): Array => { +export const getEmojisInString = (symbols: string): Array => { const regex = emojiRegex(); const matches = symbols.matchAll(regex); - return Array.from(matches).map((match) => match[0]) as Array; + return Array.from(matches).map((match) => match[0]) as Array; +}; + +export const getSymbolEmojisInString = (symbols: string): SymbolEmoji[] => { + return getEmojisInString(symbols).filter(SYMBOL_EMOJI_DATA.hasEmoji); }; /** diff --git a/src/typescript/sdk/src/mini-processor/event-groups/index.ts b/src/typescript/sdk/src/mini-processor/event-groups/index.ts index 125a92d4b..3e4137dc4 100644 --- a/src/typescript/sdk/src/mini-processor/event-groups/index.ts +++ b/src/typescript/sdk/src/mini-processor/event-groups/index.ts @@ -30,7 +30,7 @@ export type BumpEventModel = | DatabaseModels["swap_events"] | DatabaseModels["liquidity_events"]; -export type ProcessorModelsFromResponse = { +export type EventsModels = { transaction: TxnInfo; chatEvents: Array; liquidityEvents: Array; @@ -61,7 +61,7 @@ export function getEventsAsProcessorModels( events: Events, txnInfo: TxnInfo, response?: UserTransactionResponse -): ProcessorModelsFromResponse { +): EventsModels { const builders = new Map(); const marketEvents: EventWithMarket[] = [ @@ -189,8 +189,10 @@ export function getEventsAsProcessorModels( }; } -export const getEventsAsProcessorModelsFromResponse = (response: UserTransactionResponse) => { - const events = getEvents(response); +export const getEventsAsProcessorModelsFromResponse = ( + response: UserTransactionResponse, + events?: Events +) => { const txnInfo = getTxnInfo(response); - return getEventsAsProcessorModels(events, txnInfo, response); + return getEventsAsProcessorModels(events ?? getEvents(response), txnInfo, response); }; diff --git a/src/typescript/sdk/src/mini-processor/event-groups/utils.ts b/src/typescript/sdk/src/mini-processor/event-groups/utils.ts index a374ea7ed..b43979fa7 100644 --- a/src/typescript/sdk/src/mini-processor/event-groups/utils.ts +++ b/src/typescript/sdk/src/mini-processor/event-groups/utils.ts @@ -16,7 +16,7 @@ import { isSwapEvent, type Types, } from "../../types"; -import { type ProcessorModelsFromResponse, type UserLiquidityPoolsMap } from "."; +import { type EventsModels, type UserLiquidityPoolsMap } from "."; import { type AccountAddressString } from "../../emojicoin_dot_fun"; import { getLPCoinBalanceFromWriteSet } from "../parse-write-set"; @@ -68,7 +68,7 @@ const toLiquidityEventData = ( }); export const addModelsForBumpEvent = (args: { - rows: Omit & { userPools: UserLiquidityPoolsMap }; + rows: Omit & { userPools: UserLiquidityPoolsMap }; transaction: TransactionMetadata; market: MarketMetadataModel; state: StateEventData; diff --git a/src/typescript/sdk/src/types/types.ts b/src/typescript/sdk/src/types/types.ts index 9b5099e36..7ebde2d8d 100644 --- a/src/typescript/sdk/src/types/types.ts +++ b/src/typescript/sdk/src/types/types.ts @@ -16,6 +16,7 @@ import { isJSONSwapEvent, } from "./json-types"; import { type STRUCT_STRINGS } from "../utils"; +import { type Flatten } from "."; export type AnyNumberString = number | string | bigint; const strToBigInt = (data: string): bigint => BigInt(data); @@ -218,108 +219,122 @@ export type Types = { time: bigint; }; - SwapEvent: WithMarketID & + SwapEvent: Flatten< + WithMarketID & + WithVersionAndGUID & { + marketID: bigint; + time: bigint; + marketNonce: bigint; + swapper: AccountAddressString; + inputAmount: bigint; + isSell: boolean; + integrator: AccountAddressString; + integratorFeeRateBPs: number; + netProceeds: bigint; + baseVolume: bigint; + quoteVolume: bigint; + avgExecutionPriceQ64: bigint; + integratorFee: bigint; + poolFee: bigint; + startsInBondingCurve: boolean; + resultsInStateTransition: boolean; + balanceAsFractionOfCirculatingSupplyBeforeQ64: bigint; + balanceAsFractionOfCirculatingSupplyAfterQ64: bigint; + } + >; + + ChatEvent: Flatten< + WithMarketID & + WithVersionAndGUID & { + marketMetadata: Types["MarketMetadata"]; + emitTime: bigint; + emitMarketNonce: bigint; + user: AccountAddressString; + message: string; + userEmojicoinBalance: bigint; + circulatingSupply: bigint; + balanceAsFractionOfCirculatingSupplyQ64: bigint; + } + >; + + MarketRegistrationEvent: Flatten< + WithMarketID & + WithVersionAndGUID & { + marketMetadata: Types["MarketMetadata"]; + time: bigint; + registrant: AccountAddressString; + integrator: AccountAddressString; + integratorFee: bigint; + } + >; + + PeriodicStateEvent: Flatten< + WithMarketID & + WithVersionAndGUID & { + marketMetadata: Types["MarketMetadata"]; + periodicStateMetadata: Types["PeriodicStateMetadata"]; + openPriceQ64: bigint; + highPriceQ64: bigint; + lowPriceQ64: bigint; + closePriceQ64: bigint; + volumeBase: bigint; + volumeQuote: bigint; + integratorFees: bigint; + poolFeesBase: bigint; + poolFeesQuote: bigint; + numSwaps: bigint; + numChatMessages: bigint; + startsInBondingCurve: boolean; + endsInBondingCurve: boolean; + tvlPerLPCoinGrowthQ64: bigint; + } + >; + + StateEvent: Flatten< + WithMarketID & + WithVersionAndGUID & { + marketMetadata: Types["MarketMetadata"]; + stateMetadata: Types["StateMetadata"]; + clammVirtualReserves: Types["Reserves"]; + cpammRealReserves: Types["Reserves"]; + lpCoinSupply: bigint; + cumulativeStats: Types["CumulativeStats"]; + instantaneousStats: Types["InstantaneousStats"]; + lastSwap: Types["LastSwap"]; + } + >; + + GlobalStateEvent: Flatten< WithVersionAndGUID & { - marketID: bigint; - time: bigint; - marketNonce: bigint; - swapper: AccountAddressString; - inputAmount: bigint; - isSell: boolean; - integrator: AccountAddressString; - integratorFeeRateBPs: number; - netProceeds: bigint; - baseVolume: bigint; - quoteVolume: bigint; - avgExecutionPriceQ64: bigint; - integratorFee: bigint; - poolFee: bigint; - startsInBondingCurve: boolean; - resultsInStateTransition: boolean; - balanceAsFractionOfCirculatingSupplyBeforeQ64: bigint; - balanceAsFractionOfCirculatingSupplyAfterQ64: bigint; - }; - - ChatEvent: WithMarketID & - WithVersionAndGUID & { - marketMetadata: Types["MarketMetadata"]; emitTime: bigint; - emitMarketNonce: bigint; - user: AccountAddressString; - message: string; - userEmojicoinBalance: bigint; - circulatingSupply: bigint; - balanceAsFractionOfCirculatingSupplyQ64: bigint; - }; - - MarketRegistrationEvent: WithMarketID & - WithVersionAndGUID & { - marketMetadata: Types["MarketMetadata"]; - time: bigint; - registrant: AccountAddressString; - integrator: AccountAddressString; - integratorFee: bigint; - }; - - PeriodicStateEvent: WithMarketID & - WithVersionAndGUID & { - marketMetadata: Types["MarketMetadata"]; - periodicStateMetadata: Types["PeriodicStateMetadata"]; - openPriceQ64: bigint; - highPriceQ64: bigint; - lowPriceQ64: bigint; - closePriceQ64: bigint; - volumeBase: bigint; - volumeQuote: bigint; - integratorFees: bigint; - poolFeesBase: bigint; - poolFeesQuote: bigint; - numSwaps: bigint; - numChatMessages: bigint; - startsInBondingCurve: boolean; - endsInBondingCurve: boolean; - tvlPerLPCoinGrowthQ64: bigint; - }; - - StateEvent: WithMarketID & - WithVersionAndGUID & { - marketMetadata: Types["MarketMetadata"]; - stateMetadata: Types["StateMetadata"]; - clammVirtualReserves: Types["Reserves"]; - cpammRealReserves: Types["Reserves"]; - lpCoinSupply: bigint; - cumulativeStats: Types["CumulativeStats"]; - instantaneousStats: Types["InstantaneousStats"]; - lastSwap: Types["LastSwap"]; - }; - - GlobalStateEvent: WithVersionAndGUID & { - emitTime: bigint; - registryNonce: bigint; - trigger: Trigger; - cumulativeQuoteVolume: bigint; - totalQuoteLocked: bigint; - totalValueLocked: bigint; - marketCap: bigint; - fullyDilutedValue: bigint; - cumulativeIntegratorFees: bigint; - cumulativeSwaps: bigint; - cumulativeChatMessages: bigint; - }; - - LiquidityEvent: WithMarketID & - WithVersionAndGUID & { - marketID: bigint; - time: bigint; - marketNonce: bigint; - provider: AccountAddressString; - baseAmount: bigint; - quoteAmount: bigint; - lpCoinAmount: bigint; - liquidityProvided: boolean; - baseDonationClaimAmount: bigint; - quoteDonationClaimAmount: bigint; - }; + registryNonce: bigint; + trigger: Trigger; + cumulativeQuoteVolume: bigint; + totalQuoteLocked: bigint; + totalValueLocked: bigint; + marketCap: bigint; + fullyDilutedValue: bigint; + cumulativeIntegratorFees: bigint; + cumulativeSwaps: bigint; + cumulativeChatMessages: bigint; + } + >; + + LiquidityEvent: Flatten< + WithMarketID & + WithVersionAndGUID & { + marketID: bigint; + time: bigint; + marketNonce: bigint; + provider: AccountAddressString; + baseAmount: bigint; + quoteAmount: bigint; + lpCoinAmount: bigint; + liquidityProvided: boolean; + baseDonationClaimAmount: bigint; + quoteDonationClaimAmount: bigint; + } + >; // Query return type for `market_data` view. MarketDataView: { @@ -787,8 +802,8 @@ export function isLiquidityEvent(e: AnyEmojicoinEvent): e is Types["LiquidityEve return e.guid.startsWith("Liquidity"); } -// NOTE: The below code structure strongly suggests we should be using classes instead of types. -// However, we cannot use them with server components easily- hence the following code. +// Unfortunately, we can't use classes with server components or `immer` easily, so we have to write +// helper functions like these. export function getEmojicoinEventTime(e: AnyEmojicoinEvent): bigint { if (isSwapEvent(e)) return e.time; if (isChatEvent(e)) return e.emitTime; diff --git a/src/typescript/sdk/src/utils/misc.ts b/src/typescript/sdk/src/utils/misc.ts index 38d31f917..eb936ad57 100644 --- a/src/typescript/sdk/src/utils/misc.ts +++ b/src/typescript/sdk/src/utils/misc.ts @@ -1,4 +1,8 @@ -import { AccountAddress, type HexInput } from "@aptos-labs/ts-sdk"; +import { + AccountAddress, + type InputGenerateTransactionOptions, + type HexInput, +} from "@aptos-labs/ts-sdk"; import Big from "big.js"; import { type PeriodDuration, @@ -378,3 +382,16 @@ export const waitFor = async (args: { if (throwError) throw new Error(errorMessage); return false; }; + +/** + * Converts an index `i` to specified sequence number options used for transaction submission. + * @param i + * @see InputGenerateTransactionOptions + */ +export const toSequenceNumberOptions = ( + i: number +): { options: InputGenerateTransactionOptions } => ({ + options: { + accountSequenceNumber: i, + }, +}); diff --git a/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts b/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts index 1233bce6f..58512d613 100644 --- a/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts +++ b/src/typescript/sdk/src/utils/test/docker/docker-test-harness.ts @@ -23,6 +23,8 @@ const LOCAL_ENV_PATH = path.join(getGitRoot(), "src/docker", "example.local.env" const PRUNE_SCRIPT = path.join(getGitRoot(), "src/docker/utils", "prune.sh"); const PING_STATE_INTERVAL = 200; +const MAX_WAIT_TIME_SECONDS = 300; +const TMP_PID_FILE_PATH = path.join(os.tmpdir(), "emojicoin-e2e-process-id"); async function isPrimaryContainerReady(name: ContainerName): Promise { const state = await getContainerState(name); @@ -58,9 +60,6 @@ const isDataNotCorrupted = (state?: ContainerState): boolean | undefined => { return true; }; -const MAX_WAIT_TIME_SECONDS = 240; -const TMP_PID_FILE_PATH = path.join(os.tmpdir(), "emojicoin-e2e-process-id"); - export class DockerTestHarness { constructor() {} diff --git a/src/typescript/sdk/src/utils/test/test-accounts.ts b/src/typescript/sdk/src/utils/test/test-accounts.ts index 29c588041..769ccd64a 100644 --- a/src/typescript/sdk/src/utils/test/test-accounts.ts +++ b/src/typescript/sdk/src/utils/test/test-accounts.ts @@ -3,7 +3,8 @@ import testAccountData from "../../../../../docker/deployer/json/test-accounts.j export type FundedAddress = keyof typeof testAccountData; type D = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; -type FundedAccountIndex = `${D}${D}${D}`; +export type FundedAccountIndex = `${D}${D}${D}`; +export type FundedAddressIndex = FundedAccountIndex; const DIGITS = 3; export const fundedAccounts = new Map( Object.entries(testAccountData).map(([addressWith0x, privateKeyString]) => { diff --git a/src/typescript/sdk/tests/e2e/helpers/index.ts b/src/typescript/sdk/tests/e2e/helpers/index.ts new file mode 100644 index 000000000..b52385106 --- /dev/null +++ b/src/typescript/sdk/tests/e2e/helpers/index.ts @@ -0,0 +1 @@ +export * from "./misc"; diff --git a/src/typescript/sdk/tests/e2e/queries/helpers.ts b/src/typescript/sdk/tests/e2e/helpers/misc.ts similarity index 93% rename from src/typescript/sdk/tests/e2e/queries/helpers.ts rename to src/typescript/sdk/tests/e2e/helpers/misc.ts index a368894fc..92432b273 100644 --- a/src/typescript/sdk/tests/e2e/queries/helpers.ts +++ b/src/typescript/sdk/tests/e2e/helpers/misc.ts @@ -1,5 +1,3 @@ -// There are so many helper functions necessary for the new query e2e tests that it needs -// its own independent file. import { type AccountAddressInput, type UserTransactionResponse } from "@aptos-labs/ts-sdk"; import { getEvents, diff --git a/src/typescript/sdk/tests/e2e/queries/client/liquidity-pools.test.ts b/src/typescript/sdk/tests/e2e/queries/client/liquidity-pools.test.ts new file mode 100644 index 000000000..c83e462d9 --- /dev/null +++ b/src/typescript/sdk/tests/e2e/queries/client/liquidity-pools.test.ts @@ -0,0 +1,84 @@ +/* eslint-disable no-await-in-loop */ + +import { type UserTransactionResponse } from "@aptos-labs/ts-sdk"; +import { maxBigInt, ONE_APT, toSequenceNumberOptions, type SymbolEmoji } from "../../../../src"; +import { EXACT_TRANSITION_INPUT_AMOUNT } from "../../../../src/utils/test/helpers"; +import { getFundedAccounts } from "../../../../src/utils/test/test-accounts"; +import { waitForEmojicoinIndexer } from "../../../../src/indexer-v2/queries/utils"; +import { fetchMarkets, fetchUserLiquidityPools } from "../../../../src/indexer-v2/queries"; +import { LIMIT } from "../../../../src/queries"; +import { EmojicoinClient } from "../../../../src/client/emojicoin-client"; + +jest.setTimeout(20000); + +describe("queries for liquidity pools with the emojicoin client", () => { + const registrants = getFundedAccounts("046", "047"); + const emojicoin = new EmojicoinClient(); + + it("queries a user's liquidity pools with the rpc function call", async () => { + const registrant = registrants[0]; + const [swapper, provider] = [registrant, registrant]; + + const symbols: SymbolEmoji[][] = [["🏄"], ["🏄🏻"], ["🏄🏼"], ["🏄🏽"], ["🏄🏾"], ["🏄🏿"]]; + + const toSequenceNumberAndMaxGas = (n: number) => ({ + options: { + ...toSequenceNumberOptions(n).options, + maxGasAmount: ONE_APT / 100, + gasUnitPrice: 100, + }, + }); + + const responses: UserTransactionResponse[] = await Promise.all( + symbols.map((symbol, i) => + emojicoin + .register(registrant, symbol, toSequenceNumberAndMaxGas(i * 3)) + .then(() => + emojicoin + .buy(swapper, symbol, EXACT_TRANSITION_INPUT_AMOUNT) + .then(() => + emojicoin.liquidity + .provide(provider, symbol, 1000n) + .then(({ response }) => response) + ) + ) + ) + ); + + const highestVersion = maxBigInt(...responses.map(({ version }) => version)); + await waitForEmojicoinIndexer(highestVersion); + + const res = await fetchUserLiquidityPools({ provider: provider.accountAddress }); + expect(res.length).toEqual(symbols.length); + const symbolsFromQuery = new Set(res.map(({ market }) => market.symbolData.symbol)); + const symbolsFromJoinedStrings = new Set(res.map(({ market }) => market.symbolEmojis.join(""))); + expect(symbolsFromQuery).toEqual(symbolsFromJoinedStrings); + expect(symbolsFromQuery.size).toEqual(symbolsFromJoinedStrings.size); + expect(symbolsFromQuery.size).toEqual(symbols.length); + expect(symbolsFromQuery).toEqual(new Set(symbols.map((symbol) => symbol.join("")))); + }); + + it("queries all existing liquidity pools", async () => { + const registrant = registrants[1]; + const swapper = registrant; + const emojis: SymbolEmoji[] = ["🌊", "💦"]; + await emojicoin.register(registrant, emojis); + const res = await emojicoin + .buy(swapper, emojis, EXACT_TRANSITION_INPUT_AMOUNT) + .then(({ response }) => + waitForEmojicoinIndexer(response.version).then(() => + fetchMarkets({ + inBondingCurve: false, + pageSize: LIMIT, + }) + ) + ); + // If the result is not less than `LIMIT`, this test needs to be updated to paginate results. + expect(res.length).toBeLessThan(LIMIT); + + const symbol = emojis.join(""); + const poolSymbols = res.map(({ market }) => market.symbolData.symbol); + const poolSet = new Set(poolSymbols); + expect(poolSet.has(symbol)).toBe(true); + }); +}); diff --git a/src/typescript/sdk/tests/e2e/queries/client/market-state.test.ts b/src/typescript/sdk/tests/e2e/queries/client/market-state.test.ts new file mode 100644 index 000000000..791ea51fd --- /dev/null +++ b/src/typescript/sdk/tests/e2e/queries/client/market-state.test.ts @@ -0,0 +1,50 @@ +import { type SymbolEmoji } from "../../../../src"; +import { getFundedAccount } from "../../../../src/utils/test/test-accounts"; +import { waitForEmojicoinIndexer } from "../../../../src/indexer-v2/queries/utils"; +import { fetchMarketState } from "../../../../src/indexer-v2/queries"; +import { type MarketStateModel } from "../../../../src/indexer-v2/types"; +import { type JsonValue } from "../../../../src/types/json-types"; +import { EmojicoinClient } from "../../../../src/client/emojicoin-client"; + +jest.setTimeout(20000); + +describe("queries a market by market state with the emojicoin client", () => { + const registrant = getFundedAccount("048"); + const emojicoin = new EmojicoinClient(); + + it("fetches the market state for a market based on an emoji symbols array", async () => { + const emojis: SymbolEmoji[] = ["♻️", "🤕"]; + const res = await emojicoin + .register(registrant, emojis) + .then(({ response }) => waitForEmojicoinIndexer(response.version)) + .then(() => fetchMarketState({ searchEmojis: emojis })); + expect(res).not.toBeNull(); + expect(res).toBeDefined(); + expect(res!.dailyVolume).toEqual(0n); + + const results = await emojicoin.rewards.buy(registrant, emojis, 1234n).then((res) => + waitForEmojicoinIndexer(res.response.version).then(() => + fetchMarketState({ searchEmojis: emojis }).then((stateFromIndexerProcessor) => ({ + stateFromMiniProcessor: res.models.marketLatestStateEvents.at(0), + stateFromIndexerProcessor, + })) + ) + ); + + const { stateFromMiniProcessor, stateFromIndexerProcessor } = results; + expect(stateFromMiniProcessor).not.toBeNull(); + expect(stateFromIndexerProcessor).not.toBeNull(); + + // Copy over the daily volume because we can't get that field from the mini processor. + (stateFromMiniProcessor as MarketStateModel).dailyVolume = + stateFromIndexerProcessor!.dailyVolume; + // Copy over the `insertedAt` field because it's inserted at insertion time in postgres. + (stateFromMiniProcessor as MarketStateModel).transaction.insertedAt = + stateFromIndexerProcessor!.transaction.insertedAt; + + const replacer = (_: string, v: JsonValue) => (typeof v === "bigint" ? v.toString() : v); + const res1 = JSON.stringify(stateFromMiniProcessor, replacer); + const res2 = JSON.stringify(stateFromIndexerProcessor, replacer); + expect(res1).toEqual(res2); + }); +}); diff --git a/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts b/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts new file mode 100644 index 000000000..bc20feed6 --- /dev/null +++ b/src/typescript/sdk/tests/e2e/queries/client/submit.test.ts @@ -0,0 +1,384 @@ +import { + getEmojicoinMarketAddressAndTypeTags, + INTEGRATOR_ADDRESS, + INTEGRATOR_FEE_RATE_BPS, + MODULE_ADDRESS, + ONE_APT, + REWARDS_MODULE_ADDRESS, + REWARDS_MODULE_NAME, + Trigger, + zip, + type SymbolEmoji, +} from "../../../../src"; +import { getFundedAccount } from "../../../../src/utils/test/test-accounts"; +import { EmojicoinClient } from "../../../../src/client/emojicoin-client"; +import { + Aptos, + AptosConfig, + Ed25519Account, + type EntryFunctionPayloadResponse, + Network, +} from "@aptos-labs/ts-sdk"; +import { EXACT_TRANSITION_INPUT_AMOUNT } from "../../../../src/utils/test/helpers"; +import { getAptosNetwork } from "../../../../src/utils/aptos-client"; + +jest.setTimeout(15000); + +describe("all submission types for the emojicoin client", () => { + const emojicoin = new EmojicoinClient(); + const senders = [ + getFundedAccount("048"), + getFundedAccount("049"), + getFundedAccount("050"), + getFundedAccount("051"), + getFundedAccount("052"), + getFundedAccount("053"), + getFundedAccount("054"), + getFundedAccount("055"), + getFundedAccount("056"), + getFundedAccount("057"), + ]; + const symbols: Array = [ + ["✨", "🌑"], + ["✨", "🌒"], + ["✨", "🌓"], + ["✨", "🌔"], + ["✨", "🌕"], + ["✨", "🌖"], + ["✨", "🌗"], + ["✨", "🌘"], + ["✨", "🌚"], + ["✨", "🌙"], + ]; + const senderAndSymbols = zip(senders, symbols); + + const functionNames = { + registerMarket: `${MODULE_ADDRESS}::emojicoin_dot_fun::register_market`, + swap: `${MODULE_ADDRESS}::emojicoin_dot_fun::swap`, + chat: `${MODULE_ADDRESS}::emojicoin_dot_fun::chat`, + removeLiquidity: `${MODULE_ADDRESS}::emojicoin_dot_fun::remove_liquidity`, + provideLiquidity: `${MODULE_ADDRESS}::emojicoin_dot_fun::provide_liquidity`, + rewardsSwap: `${REWARDS_MODULE_ADDRESS}::${REWARDS_MODULE_NAME}::swap_with_rewards`, + }; + + const gasOptions = { + options: { + maxGasAmount: ONE_APT / 100, + gasUnitPrice: 100, + }, + }; + + it("converts emojis to a hex symbol", () => { + const [_, emojis] = senderAndSymbols[0]; + const joined = emojis.join(""); + const bytes = new TextEncoder().encode(joined); + expect(bytes).toEqual(emojicoin.utils.emojisToHexSymbol(emojis)); + expect(emojis.map((e) => new TextEncoder().encode(e))).toEqual( + emojicoin.utils.emojisToHexStrings(emojis) + ); + }); + it("converts emojis to hex strings", () => { + const [_, emojis] = senderAndSymbols[0]; + const asBytes = emojis.map((e) => new TextEncoder().encode(e)); + expect(asBytes).toEqual(emojicoin.utils.emojisToHexStrings(emojis)); + }); + + it("gets an emojicoin's market address and type tags derived from the emojis", () => { + const [_, emojis] = senderAndSymbols[0]; + const symbolBytes = emojicoin.utils.emojisToHexSymbol(emojis); + const info = getEmojicoinMarketAddressAndTypeTags({ symbolBytes }); + const { marketAddress, typeTags } = emojicoin.utils.getEmojicoinInfo(emojis); + expect(marketAddress.equals(info.marketAddress)).toBe(true); + expect(typeTags[0].toString()).toEqual(info.emojicoin.toString()); + expect(typeTags[1].toString()).toEqual(info.emojicoinLP.toString()); + }); + + it("creates the aptos client with the correct configuration settings", () => { + const config = new AptosConfig({ + network: Network.TESTNET, + }); + const aptos = new Aptos(config); + const emojicoinClient = new EmojicoinClient({ aptos }); + expect(emojicoinClient.aptos.config.network).toEqual(Network.TESTNET); + }); + + it("creates the aptos client with the correct default configuration settings", () => { + expect(emojicoin.aptos.config.network).toEqual(process.env.NEXT_PUBLIC_APTOS_NETWORK); + expect(emojicoin.aptos.config.network).toEqual(getAptosNetwork()); + }); + + it("registers a market", async () => { + const [sender, emojis] = senderAndSymbols[0]; + await emojicoin + .register(sender, emojis, gasOptions) + .then(({ response, events, registration }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.registerMarket); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toEqual(0); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(0); + expect(events.marketRegistrationEvents.length).toEqual(1); + expect(registration.event.registrant).toEqual(sender.accountAddress.toString()); + expect(registration.event.marketMetadata.emojiBytes).toEqual( + emojicoin.utils.emojisToHexSymbol(emojis) + ); + expect(registration.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(registration.model.market.trigger).toEqual(Trigger.MarketRegistration); + }); + }); + it("swap buys", async () => { + const [sender, emojis] = senderAndSymbols[1]; + await emojicoin.register(sender, emojis, gasOptions); + const inputAmount = 7654321n; + await emojicoin.buy(sender, emojis, inputAmount).then(({ response, events, swap }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.swap); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(1); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(swap.event.inputAmount).toEqual(inputAmount); + expect(swap.event.isSell).toEqual(false); + expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); + expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); + expect(swap.event.integratorFeeRateBPs).toEqual(0); + expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(swap.model.market.trigger).toEqual(Trigger.SwapBuy); + }); + }); + it("swap sells", async () => { + const [sender, emojis] = senderAndSymbols[2]; + const inputAmount = 7654321n; + await emojicoin.register(sender, emojis, gasOptions); + await emojicoin.buy(sender, emojis, inputAmount); + await emojicoin.sell(sender, emojis, inputAmount).then(({ response, events, swap }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.swap); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(1); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(swap.event.inputAmount).toEqual(inputAmount); + expect(swap.event.isSell).toEqual(true); + expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); + expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); + expect(swap.event.integratorFeeRateBPs).toEqual(0); + expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(swap.model.market.trigger).toEqual(Trigger.SwapSell); + }); + }); + + it("uses custom defaults", async () => { + const config = new AptosConfig({ network: Network.MAINNET }); + const emojicoin2 = new EmojicoinClient({ + aptos: new Aptos(config), + integrator: "0x0", + integratorFeeRateBPs: 123, + minOutputAmount: 1337n, + }); + // @ts-expect-error Checking private fields of the EmojicoinClient class instance. + const { aptos, integrator, integratorFeeRateBPs, minOutputAmount } = emojicoin2; + expect(aptos.config.network).toEqual(Network.MAINNET); + expect(integrator.toString()).toEqual("0x0"); + expect(integratorFeeRateBPs).toEqual(123); + expect(minOutputAmount).toEqual(1337n); + // @ts-expect-error Checking private fields of the test default EmojicoinClient class instance. + const { integrator: a, integratorFeeRateBPs: b, minOutputAmount: c } = emojicoin; + expect(emojicoin.aptos.config.network).not.toEqual(Network.MAINNET); + expect(a.toString()).not.toEqual("0x0"); + expect(b).not.toEqual(123); + expect(c).not.toEqual(1337n); + }); + + it("sends a chat message", async () => { + const [sender, emojis] = senderAndSymbols[3]; + const [a, b] = emojis; + const expectedMessage = [a, b, b, a].join(""); + await emojicoin.register(sender, emojis, gasOptions); + await emojicoin.chat(sender, emojis, [a, b, b, a]).then(({ response, events, chat }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.chat); + expect(events.chatEvents.length).toEqual(1); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(0); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(chat.event.message).toEqual(expectedMessage); + expect(chat.event.user).toEqual(sender.accountAddress.toString()); + expect(chat.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(chat.model.market.trigger).toEqual(Trigger.Chat); + }); + }); + it("provides liquidity", async () => { + const [sender, emojis] = senderAndSymbols[4]; + const inputAmount = 12386n; + await emojicoin.register(sender, emojis, gasOptions); + await emojicoin.buy(sender, emojis, EXACT_TRANSITION_INPUT_AMOUNT); + await emojicoin.liquidity + .provide(sender, emojis, inputAmount) + .then(({ response, events, liquidity }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.provideLiquidity); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(1); + expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(0); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(liquidity.event.quoteAmount).toEqual(inputAmount); + expect(liquidity.event.provider).toEqual(sender.accountAddress.toString()); + expect(liquidity.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(liquidity.model.market.trigger).toEqual(Trigger.ProvideLiquidity); + }); + }); + it("removes liquidity", async () => { + const [sender, emojis] = senderAndSymbols[5]; + await emojicoin.register(sender, emojis, gasOptions); + await emojicoin.buy(sender, emojis, EXACT_TRANSITION_INPUT_AMOUNT); + await emojicoin.liquidity.provide(sender, emojis, 59182n).then(({ liquidity }) => { + const lpCoinAmount = liquidity.event.lpCoinAmount; + emojicoin.liquidity + .remove(sender, emojis, lpCoinAmount) + .then(({ response, events, liquidity }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.removeLiquidity); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(1); + expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(0); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(liquidity.event.provider).toEqual(sender.accountAddress.toString()); + expect(liquidity.event.lpCoinAmount).toEqual(lpCoinAmount); + expect(liquidity.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(liquidity.model.market.trigger).toEqual(Trigger.RemoveLiquidity); + }); + }); + }); + + it("swap buys with the rewards contract", async () => { + const [sender, emojis] = senderAndSymbols[6]; + const inputAmount = 1234567n; + await emojicoin.register(sender, emojis, gasOptions); + await emojicoin.rewards.buy(sender, emojis, inputAmount).then(({ response, events, swap }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.rewardsSwap); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(1); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(swap.event.inputAmount).toEqual(inputAmount); + expect(swap.event.isSell).toEqual(false); + expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); + expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); + expect(swap.event.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); + expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(swap.model.market.trigger).toEqual(Trigger.SwapBuy); + }); + }); + it("swap sells with the rewards contract", async () => { + const [sender, emojis] = senderAndSymbols[7]; + const inputAmount = 1234567n; + await emojicoin.register(sender, emojis, gasOptions); + await emojicoin.rewards.buy(sender, emojis, inputAmount); + await emojicoin.rewards.sell(sender, emojis, inputAmount).then(({ response, events, swap }) => { + const { success } = response; + const payload = response.payload as EntryFunctionPayloadResponse; + expect(success).toBe(true); + expect(payload.function).toEqual(functionNames.rewardsSwap); + expect(events.chatEvents.length).toEqual(0); + expect(events.globalStateEvents.length).toEqual(0); + expect(events.liquidityEvents.length).toEqual(0); + expect(events.periodicStateEvents.length).toBeLessThanOrEqual(1); + expect(events.stateEvents.length).toEqual(1); + expect(events.swapEvents.length).toEqual(1); + expect(events.marketRegistrationEvents.length).toEqual(0); + expect(swap.event.inputAmount).toEqual(inputAmount); + expect(swap.event.isSell).toEqual(true); + expect(swap.event.swapper).toEqual(sender.accountAddress.toString()); + expect(swap.event.integrator).toEqual(INTEGRATOR_ADDRESS.toString()); + expect(swap.event.integratorFeeRateBPs).toEqual(INTEGRATOR_FEE_RATE_BPS); + expect(swap.model.market.emojis.map(({ emoji }) => emoji)).toEqual(emojis); + expect(swap.model.market.trigger).toEqual(Trigger.SwapSell); + }); + }); + + it("buys with a custom integrator and fee rate", async () => { + const inputAmount = 10_000_000n; + const [sender, emojis] = senderAndSymbols[8]; + const randomIntegrator = Ed25519Account.generate(); + const customIntegrator = randomIntegrator.accountAddress; + const customFeeRateBPs = 100; + const clientWithCustomDefaults = new EmojicoinClient({ + integrator: customIntegrator, + integratorFeeRateBPs: customFeeRateBPs, + }); + + await clientWithCustomDefaults.register(sender, emojis); + await clientWithCustomDefaults + .buy(sender, emojis, inputAmount) + .then(({ response, events, swap }) => { + const { success } = response; + expect(success).toBe(true); + expect(events.swapEvents.length).toEqual(1); + expect(swap.event.inputAmount).toEqual(inputAmount); + expect(swap.event.integrator).toEqual(customIntegrator.toString()); + expect(swap.event.integratorFeeRateBPs).toEqual(customFeeRateBPs); + }); + }); + + it("sells with a custom integrator and fee rate", async () => { + const inputAmount = 10_000_000n; + const [sender, emojis] = senderAndSymbols[9]; + const randomIntegrator = Ed25519Account.generate(); + const customIntegrator = randomIntegrator.accountAddress; + const customFeeRateBPs = 100; + const clientWithCustomDefaults = new EmojicoinClient({ + integrator: customIntegrator, + integratorFeeRateBPs: customFeeRateBPs, + }); + + await clientWithCustomDefaults.register(sender, emojis); + await clientWithCustomDefaults.buy(sender, emojis, inputAmount); + await clientWithCustomDefaults + .sell(sender, emojis, inputAmount) + .then(({ response, events, swap }) => { + const { success } = response; + expect(success).toBe(true); + expect(events.swapEvents.length).toEqual(1); + expect(swap.event.inputAmount).toEqual(inputAmount); + expect(swap.event.integrator).toEqual(customIntegrator.toString()); + expect(swap.event.integratorFeeRateBPs).toEqual(customFeeRateBPs); + }); + }); +}); diff --git a/src/typescript/sdk/tests/e2e/queries/empty-rows.test.ts b/src/typescript/sdk/tests/e2e/queries/empty-rows.test.ts index a63bc5cd2..cdaa536ff 100644 --- a/src/typescript/sdk/tests/e2e/queries/empty-rows.test.ts +++ b/src/typescript/sdk/tests/e2e/queries/empty-rows.test.ts @@ -20,6 +20,9 @@ describe("ensures no errors are thrown when empty rows are returned from queries for (const response of [res, state, chats, swaps]) { if (Array.isArray(response)) { + if (response.length !== 0) { + console.warn(response); + } expect(response).toBeDefined(); expect(response.length).toEqual(0); } else { diff --git a/src/typescript/sdk/tests/e2e/queries/volume.test.ts b/src/typescript/sdk/tests/e2e/queries/volume.test.ts index dae2a9ba2..666ee6d66 100644 --- a/src/typescript/sdk/tests/e2e/queries/volume.test.ts +++ b/src/typescript/sdk/tests/e2e/queries/volume.test.ts @@ -24,7 +24,7 @@ import { getOneMinutePeriodicStateEvents, getPeriodExpiryDate, getTrackerFromWriteSet, -} from "./helpers"; +} from "../helpers"; import { fetchDailyVolumeForMarket, fetchMarket1MPeriodsInLastDay, @@ -88,7 +88,15 @@ describe("queries swap_events and returns accurate swap row data", () => { const firstEvents = getEvents(first); const { marketID } = firstEvents.swapEvents[0]; - // There should be no periodic state events emitted for the first swap. + // There should be no periodic state events emitted for the first swap, but right now this test + // must be flaky due to the way that period boundaries are calculated (explained above), so + // console.warn if we encounter a periodic state event and then fail + retry. + if (firstEvents.periodicStateEvents.length > 1) { + console.warn( + "This test is inherently flaky and started at an inopportune time with regards to the " + + "PERIOD_1M boundary. It will now fail- please re-run the tests." + ); + } expect(firstEvents.periodicStateEvents.length).toEqual(0); const firstTracker = getTrackerFromWriteSet(first, marketAddress, Period.Period1M)!; diff --git a/src/typescript/sdk/tests/unit/emoji-regex.test.ts b/src/typescript/sdk/tests/unit/emoji-regex.test.ts index eea957614..91ea17abd 100644 --- a/src/typescript/sdk/tests/unit/emoji-regex.test.ts +++ b/src/typescript/sdk/tests/unit/emoji-regex.test.ts @@ -166,6 +166,8 @@ describe("tests the emojis in a string, and the emoji data for each one", () => CHAT_EMOJI_DATA.byEmojiStrict(b[2]).bytes, CHAT_EMOJI_DATA.byEmojiStrict(b[3]).bytes, ]); - expect(args.emojiIndicesSequence).toEqual([0, 1, 2, 3, 4, 5, 6, 6, 5, 4, 3, 2, 1, 0]); + expect(Array.from(args.emojiIndicesSequence)).toEqual([ + 0, 1, 2, 3, 4, 5, 6, 6, 5, 4, 3, 2, 1, 0, + ]); }); }); diff --git a/src/typescript/turbo.json b/src/typescript/turbo.json index aff1c6312..7e3a230a3 100644 --- a/src/typescript/turbo.json +++ b/src/typescript/turbo.json @@ -21,6 +21,9 @@ ], "outputs": [] }, + "check": { + "outputs": [] + }, "clean": { "outputs": [] },