import type { CoinBalance, CoinMetadata, DevInspectResults, DryRunTransactionBlockResponse, DynamicFieldPage, PaginatedEvents, PaginatedObjectsResponse, PaginatedTransactionResponse, SuiEventFilter, SuiObjectDataOptions, SuiObjectResponse, SuiObjectResponseQuery, SuiTransactionBlockResponse, TransactionFilter, } from '@mysten/sui/client' import { SuiClient } from '@mysten/sui/client' import type { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519' import type { Secp256k1Keypair } from '@mysten/sui/keypairs/secp256k1' import type { Transaction } from '@mysten/sui/transactions' import { normalizeSuiAddress } from '@mysten/sui/utils' import type { CoinAsset } from '../type/clmm' import type { DataPage, PaginationArgs, SuiObjectIdType, SuiResource } from '../type/sui' import { CachedContent, cacheTime24h, getFutureTime } from '../utils/cachedContent' import { extractStructTagFromType } from '../utils/contracts' /** * Represents a module for making RPC (Remote Procedure Call) requests. */ export class RpcModule extends SuiClient { readonly _cache: Record = {} async fetchCoinMetadata(coinType: string): Promise { const cacheKey = `coin_metadata_${coinType}` const cachedData = this.getCache(cacheKey) if (cachedData) { return cachedData } const res = await this.getCoinMetadata({ coinType }) this.updateCache(cacheKey, res) return res } /** * Gets the SUI transaction response for a given transaction digest. * @param digest - The digest of the transaction for which the SUI transaction response is requested. * @param forceRefresh - A boolean flag indicating whether to force a refresh of the response. * @returns A Promise that resolves with the SUI transaction block response or null if the response is not available. */ async getSuiTransactionResponse(digest: string): Promise { let objects try { objects = (await this.getTransactionBlock({ digest, options: { showEvents: true, showEffects: true, showBalanceChanges: true, showInput: true, showObjectChanges: true, }, })) as SuiTransactionBlockResponse } catch (error) { objects = (await this.getTransactionBlock({ digest, options: { showEvents: true, showEffects: true, }, })) as SuiTransactionBlockResponse } return objects } /** * Get events for a given query criteria * @param query * @param paginationArgs * @returns */ async queryEventsByPage(query: SuiEventFilter, paginationArgs: PaginationArgs = 'all'): Promise> { let result: any = [] let hasNextPage = true const queryAll = paginationArgs === 'all' let nextCursor = queryAll ? null : paginationArgs.cursor do { const res: PaginatedEvents = await this.queryEvents({ query, cursor: nextCursor, limit: queryAll ? null : paginationArgs.limit, }) if (res.data) { result = [...result, ...res.data] hasNextPage = res.hasNextPage nextCursor = res.nextCursor } else { hasNextPage = false } } while (queryAll && hasNextPage) return { data: result, next_cursor: nextCursor, has_next_page: hasNextPage } } async queryTransactionBlocksByPage( filter?: TransactionFilter, paginationArgs: PaginationArgs = 'all', order: 'ascending' | 'descending' | null | undefined = 'ascending' ): Promise> { let result: any = [] let hasNextPage = true const queryAll = paginationArgs === 'all' let nextCursor = queryAll ? null : paginationArgs.cursor do { const res: PaginatedTransactionResponse = await this.queryTransactionBlocks({ filter, cursor: nextCursor, order, limit: queryAll ? null : paginationArgs.limit, options: { showEvents: true }, }) if (res.data) { result = [...result, ...res.data] hasNextPage = res.hasNextPage nextCursor = res.nextCursor } else { hasNextPage = false } } while (queryAll && hasNextPage) return { data: result, next_cursor: nextCursor, has_next_page: hasNextPage } } /** * Get all objects owned by an address * @param owner * @param query * @param paginationArgs * @returns */ async getOwnedObjectsByPage( owner: string, query: SuiObjectResponseQuery, paginationArgs: PaginationArgs = 'all' ): Promise> { let result: any = [] let hasNextPage = true const queryAll = paginationArgs === 'all' let nextCursor = queryAll ? null : paginationArgs.cursor do { const res: PaginatedObjectsResponse = await this.getOwnedObjects({ owner, ...query, cursor: nextCursor, limit: queryAll ? null : paginationArgs.limit, }) if (res.data) { result = [...result, ...res.data] hasNextPage = res.hasNextPage nextCursor = res.nextCursor } else { hasNextPage = false } } while (queryAll && hasNextPage) return { data: result, next_cursor: nextCursor, has_next_page: hasNextPage } } /** * Return the list of dynamic field objects owned by an object * @param parentId * @param paginationArgs * @returns */ async getDynamicFieldsByPage(parentId: SuiObjectIdType, paginationArgs: PaginationArgs = 'all'): Promise> { let result: any = [] let hasNextPage = true const queryAll = paginationArgs === 'all' let nextCursor = queryAll ? null : paginationArgs.cursor do { const res: DynamicFieldPage = await this.getDynamicFields({ parentId, cursor: nextCursor, limit: queryAll ? null : paginationArgs.limit, }) if (res.data) { result = [...result, ...res.data] hasNextPage = res.hasNextPage nextCursor = res.nextCursor } else { hasNextPage = false } } while (queryAll && hasNextPage) return { data: result, next_cursor: nextCursor, has_next_page: hasNextPage } } /** * Batch get details about a list of objects. If any of the object ids are duplicates the call will fail * @param ids * @param options * @param limit * @returns */ async batchGetObjects(ids: SuiObjectIdType[], options?: SuiObjectDataOptions, limit = 50): Promise { let objectDataResponses: SuiObjectResponse[] = [] try { for (let i = 0; i < Math.ceil(ids.length / limit); i++) { const res = await this.multiGetObjects({ ids: ids.slice(i * limit, limit * (i + 1)), options, }) objectDataResponses = [...objectDataResponses, ...res] } } catch (error) { console.log(error) } return objectDataResponses } /** * Calculates the gas cost of a transaction block. * @param {Transaction} tx - The transaction block to calculate gas for. * @returns {Promise} - The estimated gas cost of the transaction block. * @throws {Error} - Throws an error if the sender is empty. */ async calculationTxGas(tx: Transaction): Promise { const { sender } = tx.blockData if (sender === undefined) { throw Error('sdk sender is empty') } const devResult = await this.devInspectTransactionBlock({ transactionBlock: tx, sender, }) const { gasUsed } = devResult.effects const estimateGas = Number(gasUsed.computationCost) + Number(gasUsed.storageCost) - Number(gasUsed.storageRebate) return estimateGas } /** * Sends a transaction block after signing it with the provided keypair. * * @param {Ed25519Keypair | Secp256k1Keypair} keypair - The keypair used for signing the transaction. * @param {Transaction} tx - The transaction block to send. * @returns {Promise} - The response of the sent transaction block. */ async sendTransaction(keypair: Ed25519Keypair | Secp256k1Keypair, tx: Transaction): Promise { try { const resultTxn: any = await this.signAndExecuteTransaction({ transaction: tx, signer: keypair, options: { showEffects: true, showEvents: true, }, }) return resultTxn } catch (error) { console.log('error: ', error) } return undefined } /** * Send a simulation transaction. * @param tx - The transaction block. * @param simulationAccount - The simulation account. * @param useDevInspect - A flag indicating whether to use DevInspect. Defaults to true. * @returns A promise that resolves to DevInspectResults or undefined. */ async sendSimulationTransaction(tx: Transaction, simulationAccount: string): Promise { try { tx.setSender(simulationAccount) const simulateRes = await this.dryRunTransactionBlock({ transactionBlock: await tx.build({ client: this, }), }) // const simulateRes = await this.devInspectTransactionBlock({ // transactionBlock: tx, // sender: simulationAccount, // }) return simulateRes } catch (error) { console.log('devInspectTransactionBlock error', error) } return undefined } async executeTx(keypair: Ed25519Keypair | Secp256k1Keypair, tx: Transaction, simulate: boolean): Promise { if (simulate) { const res = await this.sendSimulationTransaction(tx, normalizeSuiAddress(keypair.getPublicKey().toSuiAddress())) // console.log('executeTx: ', res.events.length > 0? res.events : res) return res!.events.length > 0 ? res!.events : res } else { const txResult = await this.sendTransaction(keypair, tx) return txResult } } /** * Gets all coin assets for the given owner and coin type. * * @param suiAddress The address of the owner. * @param coinType The type of the coin. * @returns an array of coin assets. */ async getOwnerCoinAssets(suiAddress: string, coinType?: string | null): Promise { const allCoinAsset: CoinAsset[] = [] let nextCursor: string | null | undefined = null while (true) { const allCoinObject: any = await (coinType ? this.getCoins({ owner: suiAddress, coinType, cursor: nextCursor, }) : this.getAllCoins({ owner: suiAddress, cursor: nextCursor, })) allCoinObject.data.forEach((coin: any) => { if (BigInt(coin.balance) > 0) { allCoinAsset.push({ coin_type: extractStructTagFromType(coin.coinType).source_address, coin_object_id: coin.coinObjectId, balance: BigInt(coin.balance), }) } }) nextCursor = allCoinObject.nextCursor if (!allCoinObject.hasNextPage) { break } } return allCoinAsset } /** * Gets all coin balances for the given owner and coin type. * * @param suiAddress The address of the owner. * @param coinType The type of the coin. * @returns an array of coin balances. */ async getOwnerCoinBalances(suiAddress: string, coinType?: string | null): Promise { let allCoinBalance: CoinBalance[] = [] if (coinType) { const res = await this.getBalance({ owner: suiAddress, coinType, }) allCoinBalance = [res] } else { const res = await this.getAllBalances({ owner: suiAddress, }) allCoinBalance = [...res] } return allCoinBalance } /** * Updates the cache for the given key. * * @param key The key of the cache entry to update. * @param data The data to store in the cache. * @param time The time in minutes after which the cache entry should expire. */ private updateCache(key: string, data: SuiResource, time = cacheTime24h): void { let cacheData = this._cache[key] if (cacheData) { cacheData.overdueTime = getFutureTime(time) cacheData.value = data } else { cacheData = new CachedContent(data, getFutureTime(time)) } this._cache[key] = cacheData } /** * Gets the cache entry for the given key. * * @param key The key of the cache entry to get. * @param forceRefresh Whether to force a refresh of the cache entry. * @returns The cache entry for the given key, or undefined if the cache entry does not exist or is expired. */ private getCache(key: string, forceRefresh = false): T | undefined { const cacheData = this._cache[key] const isValid = cacheData?.isValid() if (!forceRefresh && isValid) { return cacheData.value as T } if (!isValid) { delete this._cache[key] } return undefined } }