@@ -21,6 +21,7 @@ const chainOrders = require('./modules/chain_orders');
2121const chainKeys = require ( './modules/chain_keys' ) ;
2222const { OrderManager, grid : Grid , utils : OrderUtils } = require ( './modules/order' ) ;
2323const { ORDER_STATES } = require ( './modules/order/constants' ) ;
24+ const { reconcileStartupOrders, attemptResumePersistedGridByPriceMatch, decideStartupGridAction } = require ( './modules/order/startup_reconcile' ) ;
2425const accountKeys = require ( './modules/chain_keys' ) ;
2526const accountBots = require ( './modules/account_bots' ) ;
2627const { parseJsonWithComments } = accountBots ;
@@ -615,7 +616,6 @@ class DEXBot {
615616 return ;
616617 }
617618 this . _processingFill = true ;
618-
619619 try {
620620 const allFills = [ ...fills , ...this . _pendingFills ] ;
621621 this . _pendingFills = [ ] ;
@@ -697,27 +697,7 @@ class DEXBot {
697697 this . manager . logger . log ( `Processing batch of ${ validFills . length } fills using 'open' mode` , 'info' ) ;
698698 const chainOpenOrders = await chainOrders . readOpenOrders ( this . account ) ;
699699
700- // We need to accumulate results.
701- // Note: syncFromOpenOrders might return the SAME filled orders if we call it multiple times
702- // without updating the grid state in between?
703- // Actually, syncFromOpenOrders DOES update the grid (marking as filled).
704- // So subsequent calls with same open orders list will work fine?
705- // Yes, if Order A is missing, first call marks it filled. Second call Order A is filled?
706- // Wait, syncFromOpenOrders iterates order grid and checks against chain.
707- // If chainOpenOrders is constant, and grid is updated...
708- // Re-running it is redundant but safe if implementation is idempotent-ish.
709- // But why run it multiple times? Just run it ONCE without fillOp specific logging,
710- // or run it once and pass the last fillOp?
711- // The fillOp arg is only used for:
712- // "Order ${gridOrder.id} ... matched fill ${fillOp.order_id}"
713- // If we want detailed logs for each, we'd need to loop.
714- // But fundamentally, we just need to know what's missing.
715-
716700 const result = this . manager . syncFromOpenOrders ( chainOpenOrders , validFills [ 0 ] . op [ 1 ] ) ;
717- // ^ This detects ALL missing orders at once.
718- // If validFills has 5 fills, and open orders is missing 5 orders, this one call catches them all.
719- // The only downside is the log message might only reference the first fill ID or be generic.
720-
721701 if ( result . filledOrders ) allFilledOrders . push ( ...result . filledOrders ) ;
722702 if ( result . ordersNeedingCorrection ) ordersNeedingCorrection = result . ordersNeedingCorrection ;
723703 }
@@ -778,8 +758,13 @@ class DEXBot {
778758 try {
779759 this . manager . logger . log ( 'Grid regeneration triggered. Performing full grid resync...' , 'info' ) ;
780760 const readFn = ( ) => chainOrders . readOpenOrders ( this . accountId ) ;
781- const cancelFn = ( orderId ) => chainOrders . cancelOrder ( this . account , this . privateKey , orderId ) ;
782- await Grid . recalculateGrid ( this . manager , readFn , cancelFn ) ;
761+ await Grid . recalculateGrid ( this . manager , {
762+ readOpenOrdersFn : readFn ,
763+ chainOrders,
764+ account : this . account ,
765+ privateKey : this . privateKey ,
766+ config : this . config ,
767+ } ) ;
783768 accountOrders . storeMasterGrid ( this . config . botKey , Array . from ( this . manager . orders . values ( ) ) ) ;
784769
785770 if ( fs . existsSync ( this . triggerFile ) ) {
@@ -805,39 +790,52 @@ class DEXBot {
805790 this . manager . logger . log ( 'No persisted grid found. Generating new grid.' , 'info' ) ;
806791 } else {
807792 await this . manager . _initializeAssets ( ) ;
808- const chainOrderIds = new Set ( chainOpenOrders . map ( o => o . id ) ) ;
809- const hasActiveMatch = persistedGrid . some ( order => order . state === 'active' && chainOrderIds . has ( order . orderId ) ) ;
810- if ( ! hasActiveMatch ) {
811- shouldRegenerate = true ;
793+ const decision = await decideStartupGridAction ( {
794+ persistedGrid,
795+ chainOpenOrders,
796+ manager : this . manager ,
797+ logger : this . manager . logger ,
798+ storeGrid : ( orders ) => accountOrders . storeMasterGrid ( this . config . botKey , orders ) ,
799+ attemptResumeFn : attemptResumePersistedGridByPriceMatch ,
800+ } ) ;
801+ shouldRegenerate = decision . shouldRegenerate ;
802+
803+ if ( shouldRegenerate && chainOpenOrders . length === 0 ) {
812804 this . manager . logger . log ( 'Persisted grid found, but no matching active orders on-chain. Generating new grid.' , 'info' ) ;
813805 }
814806 }
815807
816808 if ( shouldRegenerate ) {
817- // Cancel unmatched on-chain orders immediately (before fetching
818- // any account totals). Use the persisted grid to determine which
819- // on-chain orders are expected; any others will be cancelled.
820- if ( ! this . config . dryRun ) {
821- try {
822- const persistedIds = new Set ( ( persistedGrid || [ ] ) . map ( o => o . orderId ) . filter ( Boolean ) ) ;
823- if ( Array . isArray ( chainOpenOrders ) && chainOpenOrders . length > 0 ) {
824- for ( const co of chainOpenOrders ) {
825- try {
826- if ( ! persistedIds . has ( co . id ) ) {
827- this . manager && this . manager . logger && this . manager . logger . log && this . manager . logger . log ( `Cancelling unmatched on-chain order ${ co . id } before initializing grid.` , 'info' ) ;
828- await chainOrders . cancelOrder ( this . account , this . privateKey , co . id ) ;
829- }
830- } catch ( err ) {
831- this . manager && this . manager . logger && this . manager . logger . log && this . manager . logger . log ( `Failed to cancel order ${ co . id } : ${ err && err . message ? err . message : err } ` , 'error' ) ;
832- }
833- }
834- }
835- } catch ( err ) {
836- this . manager && this . manager . logger && this . manager . logger . log && this . manager . logger . log ( `Failed to cancel unmatched on-chain orders: ${ err && err . message ? err . message : err } ` , 'error' ) ;
837- }
809+ // Initialize assets first
810+ await this . manager . _initializeAssets ( ) ;
811+
812+ // Always generate a full virtual grid so orders.json contains the complete grid
813+ // (virtual + spread placeholders), not only currently active on-chain orders.
814+ this . manager . logger . log ( 'Generating new grid.' , 'info' ) ;
815+ await Grid . initializeGrid ( this . manager ) ;
816+
817+ // If there are existing on-chain orders, sync them onto the new grid
818+ // using price+size matching, then reconcile counts by updating/cancelling/creating.
819+ if ( Array . isArray ( chainOpenOrders ) && chainOpenOrders . length > 0 ) {
820+ this . manager . logger . log ( `Found ${ chainOpenOrders . length } existing chain orders. Syncing them onto the new grid (price+size matching).` , 'info' ) ;
821+ const syncResult = await this . manager . synchronizeWithChain ( chainOpenOrders , 'readOpenOrders' ) ;
822+
823+ await reconcileStartupOrders ( {
824+ manager : this . manager ,
825+ config : this . config ,
826+ account : this . account ,
827+ privateKey : this . privateKey ,
828+ chainOrders,
829+ chainOpenOrders,
830+ syncResult,
831+ } ) ;
832+ } else {
833+ // No existing orders: place initial orders on-chain
834+ this . manager . logger . log ( 'No existing chain orders found. Placing initial orders.' , 'info' ) ;
835+ await this . placeInitialOrders ( ) ;
838836 }
839837
840- await this . placeInitialOrders ( ) ;
838+ accountOrders . storeMasterGrid ( this . config . botKey , Array . from ( this . manager . orders . values ( ) ) ) ;
841839 } else {
842840 this . manager . logger . log ( 'Found active session. Loading and syncing existing grid.' , 'info' ) ;
843841
@@ -892,6 +890,20 @@ class DEXBot {
892890 }
893891 }
894892
893+ // Reconcile existing on-chain orders to the configured target counts.
894+ // This logic is shared with the "regenerate" startup path and lives in one place.
895+ if ( syncResult && ( syncResult . unmatchedChainOrders || syncResult . unmatchedGridOrders ) ) {
896+ await reconcileStartupOrders ( {
897+ manager : this . manager ,
898+ config : this . config ,
899+ account : this . account ,
900+ privateKey : this . privateKey ,
901+ chainOrders,
902+ chainOpenOrders,
903+ syncResult,
904+ } ) ;
905+ }
906+
895907 // Correct any orders with price mismatches at startup
896908 if ( syncResult . ordersNeedingCorrection && syncResult . ordersNeedingCorrection . length > 0 ) {
897909 this . manager . logger . log ( `Startup: Correcting ${ syncResult . ordersNeedingCorrection . length } order(s) with price mismatch...` , 'info' ) ;
@@ -1104,6 +1116,15 @@ async function runBotInstances(botEntries, { forceDryRun = false, sourceName = '
11041116 }
11051117 }
11061118
1119+ // Fee cache is required for fill processing (getAssetFees), including offline fill reconciliation at startup.
1120+ // Initialize it once per process for the assets used by active bots.
1121+ try {
1122+ await waitForConnected ( ) ;
1123+ await OrderUtils . initializeFeeCache ( prepared . filter ( b => b . active ) , BitShares ) ;
1124+ } catch ( err ) {
1125+ console . warn ( `Fee cache initialization failed: ${ err . message } ` ) ;
1126+ }
1127+
11071128 const instances = [ ] ;
11081129 for ( const entry of prepared ) {
11091130 if ( ! entry . active ) {
@@ -1210,8 +1231,8 @@ async function stopBotByName(botName) {
12101231 * This method:
12111232 * 1. Generates a new order grid from current configuration
12121233 * 2. Persists the grid snapshot to profiles/orders.json
1213- * 3. Starts the bot with the new grid
1214- *
1234+ * 3. Starts the bot with the new grid (password authentication happens during startup)
1235+ *
12151236 * @param {string|null } botName - Name of the bot to restart, or null for all active
12161237 */
12171238async function restartBotByName ( botName ) {
@@ -1236,6 +1257,38 @@ async function restartBotByName(botName) {
12361257 try {
12371258 // Build an OrderManager with the bot config and initialize the order grid
12381259 const manager = new OrderManager ( bot ) ;
1260+
1261+ // Set account info on the manager so _fetchAccountBalancesAndSetTotals can work
1262+ if ( bot . preferredAccount ) {
1263+ manager . account = bot . preferredAccount ;
1264+ // Try to look up the account ID from the blockchain
1265+ try {
1266+ const { BitShares } = require ( './modules/bitshares_client' ) ;
1267+ const full = await BitShares . db . get_full_accounts ( [ bot . preferredAccount ] , false ) ;
1268+ if ( full && full [ 0 ] && full [ 0 ] [ 1 ] && full [ 0 ] [ 1 ] . account && full [ 0 ] [ 1 ] . account . id ) {
1269+ manager . accountId = full [ 0 ] [ 1 ] . account . id ;
1270+ } else if ( full && full [ 0 ] && String ( full [ 0 ] [ 0 ] ) . startsWith ( '1.2.' ) ) {
1271+ manager . accountId = full [ 0 ] [ 0 ] ;
1272+ }
1273+ } catch ( err ) {
1274+ console . warn ( `[dexbot restart] Could not look up account ID for '${ bot . preferredAccount } ': ${ err . message } ` ) ;
1275+ }
1276+ }
1277+
1278+ // If botFunds are percentage-based and account info is available, try to
1279+ // fetch on-chain balances first so percentages resolve correctly.
1280+ try {
1281+ const botFunds = bot && bot . botFunds ? bot . botFunds : { } ;
1282+ const needsPercent = ( v ) => typeof v === 'string' && v . includes ( '%' ) ;
1283+ if ( ( needsPercent ( botFunds . buy ) || needsPercent ( botFunds . sell ) ) && ( manager . accountId || manager . account ) ) {
1284+ if ( typeof manager . _fetchAccountBalancesAndSetTotals === 'function' ) {
1285+ await manager . _fetchAccountBalancesAndSetTotals ( ) ;
1286+ }
1287+ }
1288+ } catch ( errFetch ) {
1289+ console . warn ( `[dexbot restart] Could not fetch account totals before initializing grid for '${ bot . name } ': ${ errFetch && errFetch . message ? errFetch . message : errFetch } ` ) ;
1290+ }
1291+
12391292 // Populate assets/marketPrice and compute the virtual grid (best-effort)
12401293 try {
12411294 await Grid . initializeGrid ( manager ) ;
0 commit comments