@@ -643,6 +643,232 @@ const derivePrice = async (BitShares, symA, symB, mode) => {
643643
644644} ;
645645
646+ // ---------------------------------------------------------------------------
647+ // Fee caching and retrieval
648+ // ---------------------------------------------------------------------------
649+
650+ /**
651+ * Cache for storing fee information for all assets
652+ * Structure: {
653+ * assetSymbol: {
654+ * assetId: string,
655+ * precision: number,
656+ * marketFee: { basisPoints: number, percent: number },
657+ * takerFee: { percent: number } | null,
658+ * maxMarketFee: { raw: number, float: number }
659+ * },
660+ * BTS: { blockchain fees - see below }
661+ * }
662+ */
663+ let feeCache = { } ;
664+
665+ /**
666+ * Initialize and cache fees for all assets from bots.json configuration
667+ * Also includes BTS for blockchain fees (maker/taker order creation/cancel)
668+ *
669+ * @param {Array } botsConfig - Array of bot configurations from bots.json
670+ * @param {object } BitShares - BitShares library instance for fetching asset data
671+ * @returns {Promise<object> } The populated fee cache
672+ */
673+ async function initializeFeeCache ( botsConfig , BitShares ) {
674+ if ( ! botsConfig || ! Array . isArray ( botsConfig ) ) {
675+ throw new Error ( 'botsConfig must be an array of bot configurations' ) ;
676+ }
677+ if ( ! BitShares || ! BitShares . db ) {
678+ throw new Error ( 'BitShares library instance with db methods required' ) ;
679+ }
680+
681+ // Extract unique asset symbols from bot configurations
682+ const uniqueAssets = new Set ( [ 'BTS' ] ) ; // Always include BTS for blockchain fees
683+
684+ for ( const bot of botsConfig ) {
685+ if ( bot . assetA ) uniqueAssets . add ( bot . assetA ) ;
686+ if ( bot . assetB ) uniqueAssets . add ( bot . assetB ) ;
687+ }
688+
689+ // Fetch and cache fees for each asset
690+ for ( const assetSymbol of uniqueAssets ) {
691+ try {
692+ if ( assetSymbol === 'BTS' ) {
693+ // Special handling for BTS - fetch blockchain operation fees
694+ feeCache . BTS = await _fetchBlockchainFees ( BitShares ) ;
695+ } else {
696+ // Fetch market fees for other assets
697+ feeCache [ assetSymbol ] = await _fetchAssetMarketFees ( assetSymbol , BitShares ) ;
698+ }
699+ } catch ( error ) {
700+ console . error ( `Error caching fees for ${ assetSymbol } :` , error . message ) ;
701+ // Continue with other assets even if one fails
702+ }
703+ }
704+
705+ return feeCache ;
706+ }
707+
708+ /**
709+ * Get cached fees for a specific asset
710+ * Useful for checking if cache has been initialized
711+ *
712+ * @param {string } assetSymbol - Asset symbol (e.g., 'IOB.XRP', 'TWENTIX', 'BTS')
713+ * @returns {object|null } Fee data if cached, null otherwise
714+ */
715+ function getCachedFees ( assetSymbol ) {
716+ return feeCache [ assetSymbol ] || null ;
717+ }
718+
719+ /**
720+ * Clear the fee cache (useful for testing or refreshing)
721+ */
722+ function clearFeeCache ( ) {
723+ feeCache = { } ;
724+ }
725+
726+ /**
727+ * Get total fees (blockchain + market) for a filled order amount
728+ *
729+ * @param {string } assetSymbol - Asset symbol (e.g., 'IOB.XRP', 'TWENTIX', 'BTS')
730+ * @param {number } assetAmount - Amount of asset to calculate fees for
731+ * @returns {number } Total fee amount in the asset's native units
732+ * For BTS: blockchain fees only (creation 10% + update)
733+ * For market assets: market fee on the amount
734+ */
735+ function getAssetFees ( assetSymbol , assetAmount ) {
736+ const cachedFees = feeCache [ assetSymbol ] ;
737+
738+ if ( ! cachedFees ) {
739+ throw new Error ( `Fees not cached for ${ assetSymbol } . Call initializeFeeCache first.` ) ;
740+ }
741+
742+ assetAmount = Number ( assetAmount ) ;
743+ if ( ! Number . isFinite ( assetAmount ) || assetAmount < 0 ) {
744+ throw new Error ( `Invalid assetAmount: ${ assetAmount } ` ) ;
745+ }
746+
747+ // Special handling for BTS (blockchain fees only)
748+ if ( assetSymbol === 'BTS' ) {
749+ const orderCreationFee = cachedFees . limitOrderCreate . bts ;
750+ const orderUpdateFee = cachedFees . limitOrderUpdate . bts ;
751+ const makerNetFee = orderCreationFee * 0.1 ; // 10% of creation fee after 90% refund
752+ return makerNetFee + orderUpdateFee ;
753+ }
754+
755+ // Handle regular assets - deduct market fee from the amount received
756+ const marketFeePercent = cachedFees . marketFee ?. percent || 0 ;
757+ const marketFeeAmount = ( assetAmount * marketFeePercent ) / 100 ;
758+
759+ // Return amount after market fees are deducted
760+ return assetAmount - marketFeeAmount ;
761+ }
762+
763+ /**
764+ * Internal function to fetch blockchain operation fees
765+ */
766+ async function _fetchBlockchainFees ( BitShares ) {
767+ try {
768+ const globalProps = await BitShares . db . getGlobalProperties ( ) ;
769+ const currentFees = globalProps . parameters . current_fees ;
770+
771+ const fees = {
772+ limitOrderCreate : { raw : 0 , satoshis : 0 , bts : 0 } ,
773+ limitOrderCancel : { raw : 0 , satoshis : 0 , bts : 0 } ,
774+ limitOrderUpdate : { raw : 0 , satoshis : 0 , bts : 0 }
775+ } ;
776+
777+ // Extract fees from the parameters array
778+ for ( let i = 0 ; i < currentFees . parameters . length ; i ++ ) {
779+ const param = currentFees . parameters [ i ] ;
780+ if ( ! param || param . length < 2 ) continue ;
781+
782+ const opCode = param [ 0 ] ;
783+ const feeData = param [ 1 ] ;
784+
785+ if ( opCode === 1 && feeData . fee !== undefined ) {
786+ // Operation 1: limit_order_create
787+ fees . limitOrderCreate = {
788+ raw : feeData . fee ,
789+ satoshis : Number ( feeData . fee ) ,
790+ bts : blockchainToFloat ( feeData . fee , 5 )
791+ } ;
792+ } else if ( opCode === 2 && feeData . fee !== undefined ) {
793+ // Operation 2: limit_order_cancel
794+ fees . limitOrderCancel = {
795+ raw : feeData . fee ,
796+ satoshis : Number ( feeData . fee ) ,
797+ bts : blockchainToFloat ( feeData . fee , 5 )
798+ } ;
799+ } else if ( opCode === 77 && feeData . fee !== undefined ) {
800+ // Operation 77: limit_order_update
801+ fees . limitOrderUpdate = {
802+ raw : feeData . fee ,
803+ satoshis : Number ( feeData . fee ) ,
804+ bts : blockchainToFloat ( feeData . fee , 5 )
805+ } ;
806+ }
807+ }
808+
809+ return fees ;
810+ } catch ( error ) {
811+ throw new Error ( `Failed to fetch blockchain fees: ${ error . message } ` ) ;
812+ }
813+ }
814+
815+ /**
816+ * Internal function to fetch market fees for a specific asset
817+ */
818+ async function _fetchAssetMarketFees ( assetSymbol , BitShares ) {
819+ try {
820+ const assetData = await BitShares . db . lookupAssetSymbols ( [ assetSymbol ] ) ;
821+ if ( ! assetData || ! assetData [ 0 ] ) {
822+ throw new Error ( `Asset ${ assetSymbol } not found` ) ;
823+ }
824+
825+ const assetId = assetData [ 0 ] . id ;
826+ const fullAssets = await BitShares . db . getAssets ( [ assetId ] ) ;
827+ if ( ! fullAssets || ! fullAssets [ 0 ] ) {
828+ throw new Error ( `Could not fetch full data for ${ assetSymbol } ` ) ;
829+ }
830+
831+ const fullAsset = fullAssets [ 0 ] ;
832+ const options = fullAsset . options || { } ;
833+
834+ const marketFeeBasisPoints = options . market_fee_percent || 0 ;
835+ const marketFeePercent = marketFeeBasisPoints / 100 ;
836+
837+ // Extract taker fee from extensions
838+ let takerFeePercent = null ;
839+ if ( options . extensions && typeof options . extensions === 'object' ) {
840+ if ( options . extensions . taker_fee_percent !== undefined ) {
841+ const value = Number ( options . extensions . taker_fee_percent || 0 ) ;
842+ takerFeePercent = value / 100 ;
843+ }
844+ }
845+
846+ // Check if taker_fee_percent exists directly in options
847+ if ( takerFeePercent === null && options . taker_fee_percent !== undefined ) {
848+ const value = Number ( options . taker_fee_percent || 0 ) ;
849+ takerFeePercent = value / 100 ;
850+ }
851+
852+ return {
853+ assetId : assetId ,
854+ symbol : assetSymbol ,
855+ precision : fullAsset . precision ,
856+ marketFee : {
857+ basisPoints : marketFeeBasisPoints ,
858+ percent : marketFeePercent
859+ } ,
860+ takerFee : takerFeePercent !== null ? { percent : takerFeePercent } : null ,
861+ maxMarketFee : {
862+ raw : options . max_market_fee || 0 ,
863+ float : blockchainToFloat ( options . max_market_fee || 0 , fullAsset . precision )
864+ } ,
865+ issuer : fullAsset . issuer
866+ } ;
867+ } catch ( error ) {
868+ throw new Error ( `Failed to fetch market fees for ${ assetSymbol } : ${ error . message } ` ) ;
869+ }
870+ }
871+
646872// ---------------------------------------------------------------------------
647873// Exports
648874// ---------------------------------------------------------------------------
@@ -679,5 +905,11 @@ module.exports = {
679905 lookupAsset,
680906 deriveMarketPrice,
681907 derivePoolPrice,
682- derivePrice
908+ derivePrice,
909+
910+ // Fee caching and retrieval
911+ initializeFeeCache,
912+ getCachedFees,
913+ clearFeeCache,
914+ getAssetFees
683915} ;
0 commit comments