import type { Transaction, TransactionObjectArgument } from '@mysten/sui/transactions' import { coinWithBalance } from '@mysten/sui/transactions' import { CommonErrorCode, handleMessageError } from '../errors/errors' import type { BuildCoinResult, CoinAsset, CoinInputInterval, MultiCoinInput } from '../type/clmm' import type { SuiAddressType } from '../type/sui' import { extractStructTagFromType, normalizeCoinType } from './contracts' import { d } from './numbers' export const DEFAULT_GAS_BUDGET_FOR_SPLIT = 1000 export const DEFAULT_GAS_BUDGET_FOR_MERGE = 500 export const DEFAULT_GAS_BUDGET_FOR_TRANSFER = 100 export const DEFAULT_GAS_BUDGET_FOR_TRANSFER_SUI = 100 export const DEFAULT_GAS_BUDGET_FOR_STAKE = 1000 export const GAS_TYPE_ARG = '0x2::sui::SUI' export const GAS_TYPE_ARG_LONG = '0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI' export const GAS_SYMBOL = 'SUI' export const DEFAULT_NFT_TRANSFER_GAS_FEE = 450 export const SUI_SYSTEM_STATE_OBJECT_ID = '0x0000000000000000000000000000000000000005' /** * This class provides helper methods for working with coins. */ export class CoinAssist { /** * Get the total balance of a list of CoinAsset objects for a given coin address. * * @param objs The list of CoinAsset objects to get the total balance for. * @param coinAddress The coin address to get the total balance for. * @returns The total balance of the CoinAsset objects for the given coin address. */ public static totalBalance(objs: CoinAsset[], coinAddress: SuiAddressType): bigint { let balanceTotal = BigInt(0) objs.forEach((obj) => { if (coinAddress === obj.coin_type) { balanceTotal += BigInt(obj.balance) } }) return balanceTotal } /** * Get the CoinAsset objects for a given coin type. * * @param coinType The coin type to get the CoinAsset objects for. * @param allSuiObjects The list of all SuiMoveObjects. * @returns The CoinAsset objects for the given coin type. */ public static getCoinAssets(coinType: string, allSuiObjects: CoinAsset[]): CoinAsset[] { const coins: CoinAsset[] = [] allSuiObjects.forEach((anObj) => { if (normalizeCoinType(anObj.coin_type) === normalizeCoinType(coinType)) { coins.push(anObj) } }) return coins } /** * Get whether a coin address is a SUI coin. * * @param coinAddress The coin address to check. * @returns Whether the coin address is a SUI coin. */ public static isSuiCoin(coinAddress: SuiAddressType) { return extractStructTagFromType(coinAddress).full_address === GAS_TYPE_ARG } /** * Select the CoinAsset objects from a list of CoinAsset objects that have a balance greater than or equal to a given amount. * * @param coins The list of CoinAsset objects to select from. * @param amount The amount to select CoinAsset objects with a balance greater than or equal to. * @param exclude A list of CoinAsset objects to exclude from the selection. * @returns The CoinAsset objects that have a balance greater than or equal to the given amount. */ static selectCoinObjectIdGreaterThanOrEqual( coins: CoinAsset[], amount: bigint, exclude: string[] = [] ): { objectArray: string[]; remainCoins: CoinAsset[]; amountArray: string[] } { const selectedResult = CoinAssist.selectCoinAssetGreaterThanOrEqual(coins, amount, exclude) const objectArray = selectedResult.selectedCoins.map((item) => item.coin_object_id) const remainCoins = selectedResult.remainingCoins const amountArray = selectedResult.selectedCoins.map((item) => item.balance.toString()) return { objectArray, remainCoins, amountArray } } /** * Select the CoinAsset objects from a list of CoinAsset objects that have a balance greater than or equal to a given amount. * * @param coins The list of CoinAsset objects to select from. * @param amount The amount to select CoinAsset objects with a balance greater than or equal to. * @param exclude A list of CoinAsset objects to exclude from the selection. * @returns The CoinAsset objects that have a balance greater than or equal to the given amount. */ static selectCoinAssetGreaterThanOrEqual( coins: CoinAsset[], amount: bigint, exclude: string[] = [] ): { selectedCoins: CoinAsset[]; remainingCoins: CoinAsset[] } { const sortedCoins = CoinAssist.sortByBalance(coins.filter((c) => !exclude.includes(c.coin_object_id))) const total = CoinAssist.calculateTotalBalance(sortedCoins) if (total < amount) { return { selectedCoins: [], remainingCoins: sortedCoins } } if (total === amount) { return { selectedCoins: sortedCoins, remainingCoins: [] } } let sum = BigInt(0) const selectedCoins = [] const remainingCoins = [...sortedCoins] while (sum < total) { const target = amount - sum const coinWithSmallestSufficientBalanceIndex = remainingCoins.findIndex((c) => c.balance >= target) if (coinWithSmallestSufficientBalanceIndex !== -1) { selectedCoins.push(remainingCoins[coinWithSmallestSufficientBalanceIndex]) remainingCoins.splice(coinWithSmallestSufficientBalanceIndex, 1) break } const coinWithLargestBalance = remainingCoins.pop()! if (coinWithLargestBalance.balance > 0) { selectedCoins.push(coinWithLargestBalance) sum += coinWithLargestBalance.balance } } return { selectedCoins: CoinAssist.sortByBalance(selectedCoins), remainingCoins: CoinAssist.sortByBalance(remainingCoins) } } /** * Sort the CoinAsset objects by their balance. * * @param coins The CoinAsset objects to sort. * @returns The sorted CoinAsset objects. */ static sortByBalance(coins: CoinAsset[]): CoinAsset[] { return coins.sort((a, b) => (a.balance < b.balance ? -1 : a.balance > b.balance ? 1 : 0)) } static sortByBalanceDes(coins: CoinAsset[]): CoinAsset[] { return coins.sort((a, b) => (a.balance > b.balance ? -1 : a.balance < b.balance ? 0 : 1)) } /** * Calculate the total balance of a list of CoinAsset objects. * * @param coins The list of CoinAsset objects to calculate the total balance for. * @returns The total balance of the CoinAsset objects. */ static calculateTotalBalance(coins: CoinAsset[]): bigint { return coins.reduce((partialSum, c) => partialSum + c.balance, BigInt(0)) } public static buildCoinForAmount( tx: Transaction, allCoins: CoinAsset[], amount: bigint, coinType: string, buildVector = true, fixAmount = true ): BuildCoinResult { const coinAssets: CoinAsset[] = CoinAssist.getCoinAssets(coinType, allCoins) // mint zero coin if (amount === BigInt(0)) { return this.buildZeroValueCoin(allCoins, tx, coinType, buildVector) } const amountTotal = CoinAssist.calculateTotalBalance(coinAssets) if (amountTotal < amount) { throw new Error(`The amount(${amountTotal}) is Insufficient balance for ${coinType} , expect ${amount} `) } return this.buildCoin(tx, allCoins, coinAssets, amount, coinType, buildVector, fixAmount) } public static buildCoinWithBalance(amount: bigint, coinType: string, tx: Transaction): TransactionObjectArgument { if (amount === BigInt(0)) { if (CoinAssist.isSuiCoin(coinType)) { return tx.add(coinWithBalance({ balance: amount, useGasCoin: false })) } } return tx.add(coinWithBalance({ balance: amount, type: coinType })) } private static buildVectorCoin( tx: Transaction, allCoins: CoinAsset[], coinAssets: CoinAsset[], amount: bigint, coinType: string, fixAmount = true ): BuildCoinResult { if (CoinAssist.isSuiCoin(coinType)) { const amountCoin = tx.splitCoins(tx.gas, [tx.pure.u64(amount)]) return { selected_coins: [], target_coin: amountCoin, remain_coins: allCoins, target_coin_amount: amount.toString(), is_mint_zero_coin: false, original_spited_coin: tx.gas, } } const { original_spited_coin, target_coin, target_coin_amount, remain_coins, selected_coins } = this.buildSpitTargeCoin( tx, amount, coinAssets, fixAmount ) if (fixAmount) { return { target_coin: tx.makeMoveVec({ elements: [target_coin] }), selected_coins: selected_coins, remain_coins: remain_coins, target_coin_amount: target_coin_amount, is_mint_zero_coin: false, original_spited_coin: original_spited_coin, } } return { selected_coins: selected_coins, target_coin: tx.makeMoveVec({ elements: selected_coins.map((id) => tx.object(id)) }), remain_coins: remain_coins, target_coin_amount, is_mint_zero_coin: false, } } private static buildOneCoin( tx: Transaction, coinAssets: CoinAsset[], amount: bigint, coinType: string, fixAmount = true ): BuildCoinResult { if (CoinAssist.isSuiCoin(coinType)) { if (amount === 0n && coinAssets.length > 1) { const selectedCoinsResult = CoinAssist.selectCoinObjectIdGreaterThanOrEqual(coinAssets, amount) return { selected_coins: selectedCoinsResult.objectArray, target_coin: tx.object(selectedCoinsResult.objectArray[0]), remain_coins: selectedCoinsResult.remainCoins, target_coin_amount: selectedCoinsResult.amountArray[0], is_mint_zero_coin: false, } } const selectedCoinsResult = CoinAssist.selectCoinObjectIdGreaterThanOrEqual(coinAssets, amount) const amountCoin = tx.splitCoins(tx.gas, [tx.pure.u64(amount)]) return { selected_coins: [], target_coin: amountCoin, remain_coins: selectedCoinsResult.remainCoins, target_coin_amount: amount.toString(), is_mint_zero_coin: false, original_spited_coin: tx.gas, } } return this.buildSpitTargeCoin(tx, amount, coinAssets, fixAmount) } private static buildSpitTargeCoin(tx: Transaction, amount: bigint, coinAssets: CoinAsset[], fixAmount: boolean): BuildCoinResult { const selectedCoinsResult = CoinAssist.selectCoinObjectIdGreaterThanOrEqual(coinAssets, amount) const totalSelectedCoinAmount = selectedCoinsResult.amountArray.reduce((a, b) => Number(a) + Number(b), 0).toString() const coinObjectIds = selectedCoinsResult.objectArray const [primaryCoinA, ...mergeCoinAs] = coinObjectIds const primaryCoinAObject = tx.object(primaryCoinA) let targetCoin: any = primaryCoinAObject const targetCoinAmount = selectedCoinsResult.amountArray.reduce((a, b) => Number(a) + Number(b), 0).toString() let originalSpitedCoin if (mergeCoinAs.length > 0) { tx.mergeCoins( primaryCoinAObject, mergeCoinAs.map((coin) => tx.object(coin)) ) } if (fixAmount && Number(totalSelectedCoinAmount) > Number(amount)) { targetCoin = tx.splitCoins(primaryCoinAObject, [tx.pure.u64(amount)]) originalSpitedCoin = primaryCoinAObject } return { original_spited_coin: originalSpitedCoin, target_coin: targetCoin, target_coin_amount: targetCoinAmount, remain_coins: selectedCoinsResult.remainCoins, selected_coins: selectedCoinsResult.objectArray, is_mint_zero_coin: false, } } private static buildCoin( tx: Transaction, allCoins: CoinAsset[], coinAssets: CoinAsset[], amount: bigint, coinType: string, buildVector = true, fixAmount = true ): BuildCoinResult { if (buildVector) { return this.buildVectorCoin(tx, allCoins, coinAssets, amount, coinType, fixAmount) } return this.buildOneCoin(tx, coinAssets, amount, coinType, fixAmount) } private static buildZeroValueCoin(allCoins: CoinAsset[], tx: Transaction, coinType: string, buildVector = true): BuildCoinResult { const zeroCoin = this.callMintZeroValueCoin(tx, coinType) let targetCoin: any if (buildVector) { targetCoin = tx.makeMoveVec({ elements: [zeroCoin] }) } else { targetCoin = zeroCoin } return { target_coin: targetCoin, remain_coins: allCoins, selected_coins: [], is_mint_zero_coin: true, target_coin_amount: '0', } } public static buildCoinForAmountInterval( tx: Transaction, allCoins: CoinAsset[], amounts: CoinInputInterval, coinType: string, buildVector = true, fixAmount = true ): BuildCoinResult { const coinAssets: CoinAsset[] = CoinAssist.getCoinAssets(coinType, allCoins) if (amounts.amount_first === BigInt(0)) { if (coinAssets.length > 0) { return this.buildCoin(tx, [...allCoins], [...coinAssets], amounts.amount_first, coinType, buildVector, fixAmount) } return this.buildZeroValueCoin(allCoins, tx, coinType, buildVector) } const amountTotal = CoinAssist.calculateTotalBalance(coinAssets) if (amountTotal >= amounts.amount_first) { return this.buildCoin(tx, [...allCoins], [...coinAssets], amounts.amount_first, coinType, buildVector, fixAmount) } if (amountTotal < amounts.amount_second) { throw new Error(`The amount(${amountTotal}) is Insufficient balance for ${coinType} , expect ${amounts.amount_second} `) } return this.buildCoin(tx, [...allCoins], [...coinAssets], amounts.amount_second, coinType, buildVector, fixAmount) } public static callMintZeroValueCoin = (txb: Transaction, coinType: string) => { return txb.moveCall({ target: '0x2::coin::zero', typeArguments: [coinType], }) } public static fromBalance(balance: TransactionObjectArgument, coinType: string, tx: Transaction): TransactionObjectArgument { const coin = tx.moveCall({ target: `0x2::coin::from_balance`, typeArguments: [coinType], arguments: [balance], }) return coin } public static getCoinAmountObjId(coinInput: MultiCoinInput, amount: string): TransactionObjectArgument { const coinObj = coinInput.amount_coin_array.find((coin) => { if (!coin.used && d(coin.amount).eq(amount)) { coin.used = true return true } return false }) if (!coinObj) { return handleMessageError(CommonErrorCode.CoinNotFound, `Coin not found for ${amount} ${coinInput.coin_type}`) } return coinObj.coin_object_id } public static buildMultiCoinInput(tx: Transaction, allCoinAssets: CoinAsset[], coinType: string, amountArr: bigint[]): MultiCoinInput { const coinAssets = CoinAssist.getCoinAssets(coinType, allCoinAssets) if (CoinAssist.isSuiCoin(coinType)) { const amountCoins = tx.splitCoins( tx.gas, amountArr.map((amount) => tx.pure.u64(amount)) ) const amountCoinArray = amountArr.map((amount, index) => { return { coin_object_id: amountCoins[index], amount: amount.toString(), used: false, } }) return { amount_coin_array: amountCoinArray, coin_type: coinType, remain_coins: coinAssets, } } const totalAmount = amountArr.reduce((acc, curr) => acc + curr, BigInt(0)) const selectedCoinsResult = CoinAssist.selectCoinObjectIdGreaterThanOrEqual(coinAssets, totalAmount) if (selectedCoinsResult.objectArray.length === 0) { return handleMessageError( CommonErrorCode.InsufficientBalance, `No enough coins for ${coinType} expect ${totalAmount} actual ${CoinAssist.calculateTotalBalance(coinAssets)}` ) } const [targetCoin, ...otherCoins] = selectedCoinsResult.objectArray if (otherCoins.length > 0) { tx.mergeCoins(targetCoin, [...otherCoins]) } const amountCoins = tx.splitCoins( targetCoin, amountArr.map((amount) => tx.pure.u64(amount)) ) const amountCoinArray = amountArr.map((amount, index) => { return { coin_object_id: amountCoins[index], amount: amount.toString(), used: false, } }) return { amount_coin_array: amountCoinArray, remain_coins: selectedCoinsResult.remainCoins, coin_type: coinType, } } }