diff --git a/src/index.ts b/src/index.ts index 2efdfe6..c5477d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export * from "./managers/types"; export * from "./managers/dca/DCAManager"; export * from "./managers/dca/types"; export * from "./managers/dca/utils"; +export * from "./managers/FeeManager"; // Providers (common & utils) export * from "./providers/common"; @@ -41,10 +42,13 @@ export * from "./storages/RedisStorage"; export * from "./storages/InMemoryStorage"; export * from "./storages/types"; export * from "./storages/utils/typeguards"; +export * from "./storages/utils/getIsDcaTradingCache"; +export * from "./storages/utils/storeIsDcaTradingCache"; // Misc export { SUI_DECIMALS, isValidSuiAddress } from "@mysten/sui.js/utils"; export { TransactionBlock, isTransactionBlock } from "@mysten/sui.js/transactions"; +export { Ed25519Keypair } from "@mysten/sui.js/keypairs/ed25519"; // Launchpad export * from "./launchpad/surfdog/surfdog"; diff --git a/src/managers/dca/DCAManager.ts b/src/managers/dca/DCAManager.ts index 07ccf18..7599b26 100644 --- a/src/managers/dca/DCAManager.ts +++ b/src/managers/dca/DCAManager.ts @@ -2,6 +2,7 @@ import { EventId, PaginatedEvents, SuiClient, SuiEvent } from "@mysten/sui.js/client"; import { TransactionBlock } from "@mysten/sui.js/transactions"; import { SUI_CLOCK_OBJECT_ID } from "@mysten/sui.js/utils"; +import BigNumber from "bignumber.js"; import { MAX_BATCH_EVENTS_PER_QUERY_EVENTS_REQUEST } from "../../providers/common"; import { getAllObjects } from "../../providers/utils/getAllObjects"; import { GetTransactionType } from "../../transactions/types"; @@ -12,6 +13,7 @@ import { CreateDCAInitTransactionArgs, DCACreateEventParsedJson, DCAObject, + DCAObjectFields, GetDCAAddGasBudgetTransactionArgs, GetDCADepositBaseTransactionArgs, GetDCAIncreaseOrdersRemainingTransactionArgs, @@ -26,7 +28,6 @@ import { SuiEventDCACreate, } from "./types"; import { filterValidDCAObjects, getBaseQuoteCoinTypesFromDCAType, hasMinMaxPriceParams } from "./utils"; -import BigNumber from "bignumber.js"; import { DCA_CONFIG } from "./config"; /** @@ -140,6 +141,12 @@ export class DCAManagerSingleton { return dcaList; } + public async getActiveDCAsFieldsByPackage(): Promise { + const dcasByPackage = await this.getDCAsByPackage(); + + return dcasByPackage.filter((dca) => dca.fields.active === true).map((dca) => dca.fields); + } + public async getDCAEventsByUser({ publicKey }: { publicKey: string }): Promise { // TODO: Move that logic into separate util (e.g. `fetchEventsByUser`) // TODO: Unify that method with `getDCAEventsByPackage` diff --git a/src/managers/dca/types.ts b/src/managers/dca/types.ts index 5ccdea3..dcd148c 100644 --- a/src/managers/dca/types.ts +++ b/src/managers/dca/types.ts @@ -11,6 +11,22 @@ export enum DCATimescale { Months = 5, } +export const SECOND_IN_MS = 1_000; +export const MINUTE_IN_MS = 60 * SECOND_IN_MS; +export const HOUR_IN_MS = 60 * MINUTE_IN_MS; +export const DAY_IN_MS = 24 * HOUR_IN_MS; +export const WEEK_IN_MS = 7 * DAY_IN_MS; +export const MONTH_IN_MS = 30 * DAY_IN_MS; + +export const DCATimescaleToMillisecondsMap = new Map([ + [DCATimescale.Seconds, SECOND_IN_MS], + [DCATimescale.Minutes, MINUTE_IN_MS], + [DCATimescale.Hours, HOUR_IN_MS], + [DCATimescale.Days, DAY_IN_MS], + [DCATimescale.Weeks, WEEK_IN_MS], + [DCATimescale.Months, MONTH_IN_MS], +]); + export type GetDCAInitTransactionArgs = { baseCoinType: string; quoteCoinType: string; @@ -162,11 +178,13 @@ export type DCAContentFields = { }; }; +export type DCAObjectFields = DCAContentFields & { + base_coin_type: string; + quote_coin_type: string; +}; + export interface DCAObject extends DCAContent { - fields: DCAContentFields & { - base_coin_type: string; - quote_coin_type: string; - }; + fields: DCAObjectFields; } export interface DCAResponseData extends SuiObjectData { diff --git a/src/managers/dca/utils.ts b/src/managers/dca/utils.ts index b1f756d..9c36851 100644 --- a/src/managers/dca/utils.ts +++ b/src/managers/dca/utils.ts @@ -1,8 +1,9 @@ /* eslint-disable require-jsdoc */ -import { MoveStruct, SuiParsedData, SuiObjectResponse } from "@mysten/sui.js/client"; -import { DCAContent, DCAContentFields, DCAResponse } from "./types"; +import { SuiObjectResponse, SuiParsedData } from "@mysten/sui.js/client"; +import BigNumber from "bignumber.js"; import { TOKEN_ADDRESS_BASE_REGEX } from "../../providers/common"; +import { DCAContent, DCAContentFields, DCAResponse, DCATimescaleToMillisecondsMap } from "./types"; import { Argument } from "./txBlock"; import { DCA_CONFIG } from "./config"; @@ -13,39 +14,35 @@ export function feeAmount(amount: number): number { return scaledFee / 1_000_000; } -export function isValidDCAFields(fields: MoveStruct): fields is DCAContentFields { - const expectedKeys: (keyof DCAContentFields)[] = [ - "active", - "input_balance", - "delegatee", - "every", - "gas_budget", - "id", - "last_time_ms", - "owner", - "remaining_orders", - "split_allocation", - "start_time_ms", - "time_scale", - "trade_params", - ]; - +export function isValidDCAFields(fields: unknown): fields is DCAContentFields { return ( - expectedKeys.every((key) => key in fields) && - // the "active" in fields is the ts-check bypass for MoveStruct type + typeof fields === "object" && + fields !== null && "active" in fields && typeof fields.active === "boolean" && + "input_balance" in fields && typeof fields.input_balance === "string" && + "delegatee" in fields && typeof fields.delegatee === "string" && + "every" in fields && typeof fields.every === "string" && + "gas_budget" in fields && typeof fields.gas_budget === "string" && - typeof fields.id === "object" && // Assuming id is always an object + "id" in fields && + typeof fields.id === "object" && + "last_time_ms" in fields && typeof fields.last_time_ms === "string" && + "owner" in fields && typeof fields.owner === "string" && + "remaining_orders" in fields && typeof fields.remaining_orders === "string" && + "split_allocation" in fields && typeof fields.split_allocation === "string" && + "start_time_ms" in fields && typeof fields.start_time_ms === "string" && + "time_scale" in fields && typeof fields.time_scale === "number" && + "trade_params" in fields && typeof fields.trade_params === "object" && fields.trade_params !== null && "type" in fields.trade_params && @@ -61,6 +58,12 @@ export function isValidDCAFields(fields: MoveStruct): fields is DCAContentFields ); } +export function isValidDCAFieldsArray(data: unknown): data is DCAContentFields[] { + if (!Array.isArray(data)) return false; + + return data.every((item) => isValidDCAFields(item)); +} + export function isDCAContent(data: SuiParsedData | null): data is DCAContent { return ( !!data && @@ -113,6 +116,16 @@ export function hasMinMaxPriceParams(params: { return params.minPrice !== undefined && params.maxPrice !== undefined; } +export function getMillisecondsByDcaEveryParams(every: string, timeScale: number): number { + const milliseconds = DCATimescaleToMillisecondsMap.get(timeScale); + + if (milliseconds === undefined) { + throw new Error(); + } + + return new BigNumber(every).multipliedBy(milliseconds).toNumber(); +} + export const fromArgument = (arg: Argument, idx: number) => { // console.log(`Processing argument at index ${idx}:`, arg); diff --git a/src/storages/RedisStorage.ts b/src/storages/RedisStorage.ts index b9468ad..2056ec4 100644 --- a/src/storages/RedisStorage.ts +++ b/src/storages/RedisStorage.ts @@ -42,6 +42,7 @@ export class RedisStorageSingleton implements IStorage { return RedisStorageSingleton._instance; } + // TODO: Refactor this method to set specified types of value to avoid `provider` accidental property non-indication /** * Sets cache data in Redis. * @param {SetCacheParams} params - Parameters containing provider, property, and value. @@ -50,7 +51,17 @@ export class RedisStorageSingleton implements IStorage { */ public async setCache(params: SetCacheParams): Promise { const { provider, property, value } = params; - const key = `${provider}.${property}.${RedisStorageSingleton.version}`; + let key; + + // When `provider` is not defined (e.g. for StorageProperty.DCAs), don't use it in `key` + // TODO: Make this clearer and more graceful + if (provider === undefined) { + key = `${property}.${RedisStorageSingleton.version}`; + } else { + // When `provider` is defined (e.g. for StorageProperty.Coins), use it in `key` + key = `${provider}.${property}.${RedisStorageSingleton.version}`; + } + const stringifiedValue: string = JSON.stringify(value); const setResult = await this.client.set(key, stringifiedValue); @@ -61,6 +72,7 @@ export class RedisStorageSingleton implements IStorage { } } + // TODO: Refactor this method to get specified types of value to avoid `provider` accidental property non-indication /** * Retrieves cache data from Redis. * @param {GetCacheParams} params - Parameters containing provider and property. @@ -69,7 +81,17 @@ export class RedisStorageSingleton implements IStorage { */ public async getCache(params: GetCacheParams): Promise { const { provider, property } = params; - const key = `${provider}.${property}.${RedisStorageSingleton.version}`; + let key; + + // When `provider` is not defined (e.g. for StorageProperty.DCAs), don't use it in `key` + // TODO: Make this clearer and more graceful + if (provider === undefined) { + key = `${property}.${RedisStorageSingleton.version}`; + } else { + // When `provider` is defined (e.g. for StorageProperty.Coins), use it in `key` + key = `${provider}.${property}.${RedisStorageSingleton.version}`; + } + const value = await this.client.get(key); if (value === null) { diff --git a/src/storages/types.ts b/src/storages/types.ts index c9b47e0..1bf7c4b 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -1,11 +1,12 @@ import { createClient } from "redis"; +import { DCAObjectFields } from "../managers/dca/types"; import { CommonCoinData } from "../managers/types"; +import { CetusPathForStorage } from "../providers/cetus/types"; import { ShortCoinMetadata } from "../providers/flowx/types"; import { ShortPoolData } from "../providers/turbos/types"; import { CommonPoolData } from "../providers/types"; import { InMemoryStorageSingleton } from "./InMemoryStorage"; import { RedisStorageSingleton } from "./RedisStorage"; -import { CetusPathForStorage } from "../providers/cetus/types"; export type Storage = InMemoryStorageSingleton | RedisStorageSingleton; @@ -15,7 +16,7 @@ export interface IStorage { } export type GetCacheParams = { - provider: string; + provider?: string; property: StorageProperty; }; @@ -29,6 +30,8 @@ export enum StorageProperty { Pools = "pools", CoinsMetadata = "coinsMetadata", CetusPaths = "cetusPaths", + DCAs = "dcas", + IsDCATrading = "isDcaTrading", } export type StorageValue = @@ -37,6 +40,8 @@ export type StorageValue = | { value: ShortCoinMetadata[]; timestamp: string } | { value: ShortPoolData[]; timestamp: string } | { value: CetusPathForStorage[]; timestamp: string } + | { value: DCAObjectFields[]; timestamp: string } + | { value: boolean; timestamp: string } | null; export type RedisStorageClient = ReturnType; diff --git a/src/storages/utils/getCetusPathsCache.ts b/src/storages/utils/getCetusPathsCache.ts index fdf0e9e..e6cc068 100644 --- a/src/storages/utils/getCetusPathsCache.ts +++ b/src/storages/utils/getCetusPathsCache.ts @@ -41,10 +41,10 @@ export const getCetusPathsCache = async ({ } else if (paths === null) { console.warn(`[getCetusPathsCache] ${provider} Received empty paths from strorage, paths === null `); } else { - const stringifiedPath: string = JSON.stringify(paths.value[0]); + const stringifiedPaths: string = JSON.stringify(paths.value); throw new Error( `[${provider}] prefillCaches: paths from storage are not (PathLink[] or null). ` + - `Example of path: ${stringifiedPath}`, + `Paths from storage: ${stringifiedPaths}`, ); } diff --git a/src/storages/utils/getCoinsCache.ts b/src/storages/utils/getCoinsCache.ts index 452abda..cb71f33 100644 --- a/src/storages/utils/getCoinsCache.ts +++ b/src/storages/utils/getCoinsCache.ts @@ -36,10 +36,10 @@ export const getCoinsCache = async ({ } else if (coins === null) { console.warn(`[getCoinsCache] ${provider} Received empty coins from strorage, coins === null `); } else { - const stringifiedCoin: string = JSON.stringify(coins.value[0]); + const stringifiedCoins: string = JSON.stringify(coins.value); throw new Error( `[${provider}] prefillCaches: coins from storage are not (CommonCoinData[] or null). ` + - `Example of coin: ${stringifiedCoin}`, + `Coins from storage: ${stringifiedCoins}`, ); } diff --git a/src/storages/utils/getCoinsMetadataCache.ts b/src/storages/utils/getCoinsMetadataCache.ts index 8dab582..1190004 100644 --- a/src/storages/utils/getCoinsMetadataCache.ts +++ b/src/storages/utils/getCoinsMetadataCache.ts @@ -1,5 +1,5 @@ import { ExtractedCoinMetadataType } from "../../providers/flowx/types"; -import { StorageValue, StorageProperty, Storage } from "../types"; +import { Storage, StorageProperty, StorageValue } from "../types"; import { isShortCoinMetadataArray } from "./typeguards"; /** @@ -36,10 +36,10 @@ export async function getCoinsMetadataCache({ coinsMetadataCache === null `, ); } else { - const stringifiedCoinMetadata: string = JSON.stringify(coinsMetadata.value[0]); + const stringifiedCoinMetadata: string = JSON.stringify(coinsMetadata.value); throw new Error( `[${provider}] getCoinsMetadataCache: coins metadata from storage is not ` + - `(ExtractedCoinMetadataType[] or null). Example of coin metadata: ${stringifiedCoinMetadata}`, + `(ExtractedCoinMetadataType[] or null). Coin metadata from storage: ${stringifiedCoinMetadata}`, ); } diff --git a/src/storages/utils/getIsDcaTradingCache.ts b/src/storages/utils/getIsDcaTradingCache.ts new file mode 100644 index 0000000..c90517e --- /dev/null +++ b/src/storages/utils/getIsDcaTradingCache.ts @@ -0,0 +1,24 @@ +/* eslint-disable require-jsdoc */ + +import { Storage, StorageProperty, StorageValue } from "../types"; +import { isDcaIsTradingField } from "./typeguards"; + +export async function getIsDcaTradingCache({ storage }: { storage: Storage }): Promise { + let isDcaTrading = false; + + const isDcaTradingCache: StorageValue = await storage.getCache({ property: StorageProperty.IsDCATrading }); + + if (isDcaIsTradingField(isDcaTradingCache?.value)) { + isDcaTrading = isDcaTradingCache.value; + } else if (isDcaTradingCache === null) { + console.warn("[getIsDcaTradingCache] Received empty isDcaTrading from strorage, isDcaTrading === null"); + } else { + const stringifiedIsDcaTrading: string = JSON.stringify(isDcaTradingCache.value); + throw new Error( + "[getIsDcaTradingCache] isDcaTrading from storage is not boolean or null. " + + `Value from storage: ${stringifiedIsDcaTrading}`, + ); + } + + return isDcaTrading; +} diff --git a/src/storages/utils/getPathsCache.ts b/src/storages/utils/getPathsCache.ts index 4c4b648..0a555b4 100644 --- a/src/storages/utils/getPathsCache.ts +++ b/src/storages/utils/getPathsCache.ts @@ -37,10 +37,10 @@ export const getPathsCache = async ({ } else if (paths === null) { console.warn(`[getPathsCache] ${provider} Received empty paths from strorage, paths === null `); } else { - const stringifiedPath: string = JSON.stringify(paths.value[0]); + const stringifiedPaths: string = JSON.stringify(paths.value); throw new Error( `[${provider}] prefillCaches: paths from storage are not (CommonPoolData[] or null). ` + - `Example of path: ${stringifiedPath}`, + `Paths from storage: ${stringifiedPaths}`, ); } diff --git a/src/storages/utils/getPoolsCache.ts b/src/storages/utils/getPoolsCache.ts index 772146a..13fc509 100644 --- a/src/storages/utils/getPoolsCache.ts +++ b/src/storages/utils/getPoolsCache.ts @@ -1,6 +1,6 @@ +import { ShortPoolData } from "../../providers/turbos/types"; import { Storage, StorageProperty, StorageValue } from "../types"; import { isShortPoolDataArray } from "./typeguards"; -import { ShortPoolData } from "../../providers/turbos/types"; /** * Returns pools cache from storage. If cache is not up to date, empty array is returned. @@ -33,10 +33,10 @@ export const getPoolsCache = async ({ } else if (pools === null) { console.warn(`[getPoolsCache] ${provider} Received empty pools from strorage, pools === null `); } else { - const stringifiedPool: string = JSON.stringify(pools.value[0]); + const stringifiedPools: string = JSON.stringify(pools.value); throw new Error( `[${provider}] getPoolsCache: pools from storage are not ` + - `(ShortPoolData[] or null). Example of pool: ${stringifiedPool}`, + `(ShortPoolData[] or null). Pools from storage: ${stringifiedPools}`, ); } diff --git a/src/storages/utils/storeIsDcaTradingCache.ts b/src/storages/utils/storeIsDcaTradingCache.ts new file mode 100644 index 0000000..317d597 --- /dev/null +++ b/src/storages/utils/storeIsDcaTradingCache.ts @@ -0,0 +1,24 @@ +/* eslint-disable require-jsdoc */ + +import { Storage, StorageProperty } from "../types"; + +export async function storeIsDcaTradingCache({ + storage, + isDcaTrading, +}: { + storage: Storage; + isDcaTrading: boolean; +}): Promise { + try { + const timestamp = Date.now().toString(); + + await storage.setCache({ + property: StorageProperty.IsDCATrading, + value: { value: isDcaTrading, timestamp }, + }); + } catch (error) { + console.error("[storeIsDcaTrading] Error occured while storing:", error); + + throw error; + } +} diff --git a/src/storages/utils/typeguards.ts b/src/storages/utils/typeguards.ts index 39e1ac2..9ecc2dc 100644 --- a/src/storages/utils/typeguards.ts +++ b/src/storages/utils/typeguards.ts @@ -1,5 +1,6 @@ /* eslint-disable require-jsdoc */ +import { isValidDCAFieldsArray } from "../.."; import { CommonCoinData } from "../../managers/types"; import { CetusPathForStorage } from "../../providers/cetus/types"; import { ShortCoinMetadata } from "../../providers/flowx/types"; @@ -16,7 +17,10 @@ export function isStorageValue(data: unknown): data is StorageValue { (isCommonCoinDataArray(data.value) || isCommonPoolDataArray(data.value) || isShortCoinMetadataArray(data.value) || - isShortPoolDataArray(data.value)) + isShortPoolDataArray(data.value) || + isCetusPathForStorageArray(data.value) || + isValidDCAFieldsArray(data.value) || + isDcaIsTradingField(data.value)) ); } @@ -102,3 +106,7 @@ export function isCetusPathForStorageArray(data: unknown): data is CetusPathForS ), ); } + +export function isDcaIsTradingField(data: unknown): data is boolean { + return typeof data === "boolean"; +}