dexbot 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/chart_utils.d.ts +11 -9
- package/dist/analysis/chart_utils.d.ts.map +1 -1
- package/dist/analysis/chart_utils.js +21 -16
- package/dist/analysis/chart_utils.js.map +1 -1
- package/dist/analysis/math_utils.d.ts +4 -13
- package/dist/analysis/math_utils.d.ts.map +1 -1
- package/dist/analysis/math_utils.js +16 -18
- package/dist/analysis/math_utils.js.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts +3 -7
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +38 -34
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
- package/dist/analysis/trend_detection/hurst_analyzer.d.ts +1 -4
- package/dist/analysis/trend_detection/hurst_analyzer.d.ts.map +1 -1
- package/dist/analysis/trend_detection/hurst_analyzer.js +10 -7
- package/dist/analysis/trend_detection/hurst_analyzer.js.map +1 -1
- package/dist/analysis/trend_detection/kalman_trend_analyzer.d.ts +6 -6
- package/dist/analysis/trend_detection/kalman_trend_analyzer.d.ts.map +1 -1
- package/dist/analysis/trend_detection/kalman_trend_analyzer.js +23 -15
- package/dist/analysis/trend_detection/kalman_trend_analyzer.js.map +1 -1
- package/dist/analysis/trend_detection/kalman_velocity_smoothing.d.ts +12 -13
- package/dist/analysis/trend_detection/kalman_velocity_smoothing.d.ts.map +1 -1
- package/dist/analysis/trend_detection/kalman_velocity_smoothing.js +16 -16
- package/dist/analysis/trend_detection/kalman_velocity_smoothing.js.map +1 -1
- package/dist/analysis/trend_detection/permutation_entropy_analyzer.d.ts +1 -4
- package/dist/analysis/trend_detection/permutation_entropy_analyzer.d.ts.map +1 -1
- package/dist/analysis/trend_detection/permutation_entropy_analyzer.js +10 -7
- package/dist/analysis/trend_detection/permutation_entropy_analyzer.js.map +1 -1
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +6 -13
- package/dist/bot.js.map +1 -1
- package/dist/credential-daemon.d.ts.map +1 -1
- package/dist/credential-daemon.js +74 -35
- package/dist/credential-daemon.js.map +1 -1
- package/dist/dexbot.d.ts.map +1 -1
- package/dist/dexbot.js +16 -15
- package/dist/dexbot.js.map +1 -1
- package/dist/market_adapter/ama_signal_runner.d.ts +1 -1
- package/dist/market_adapter/ama_signal_runner.d.ts.map +1 -1
- package/dist/market_adapter/ama_signal_runner.js +8 -20
- package/dist/market_adapter/ama_signal_runner.js.map +1 -1
- package/dist/market_adapter/candle_utils.d.ts +2 -10
- package/dist/market_adapter/candle_utils.d.ts.map +1 -1
- package/dist/market_adapter/candle_utils.js +7 -8
- package/dist/market_adapter/candle_utils.js.map +1 -1
- package/dist/market_adapter/core/asymmetric_bounds.d.ts +12 -7
- package/dist/market_adapter/core/asymmetric_bounds.d.ts.map +1 -1
- package/dist/market_adapter/core/asymmetric_bounds.js +4 -5
- package/dist/market_adapter/core/asymmetric_bounds.js.map +1 -1
- package/dist/market_adapter/core/config_normalizers.d.ts +4 -9
- package/dist/market_adapter/core/config_normalizers.d.ts.map +1 -1
- package/dist/market_adapter/core/config_normalizers.js +9 -9
- package/dist/market_adapter/core/config_normalizers.js.map +1 -1
- package/dist/market_adapter/core/kibana_candles.d.ts +16 -20
- package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_candles.js +16 -15
- package/dist/market_adapter/core/kibana_candles.js.map +1 -1
- package/dist/market_adapter/core/kibana_client.d.ts +8 -11
- package/dist/market_adapter/core/kibana_client.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_client.js +13 -11
- package/dist/market_adapter/core/kibana_client.js.map +1 -1
- package/dist/market_adapter/core/kibana_market_candles.d.ts +5 -12
- package/dist/market_adapter/core/kibana_market_candles.d.ts.map +1 -1
- package/dist/market_adapter/core/kibana_market_candles.js +12 -21
- package/dist/market_adapter/core/kibana_market_candles.js.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.d.ts +75 -53
- package/dist/market_adapter/core/market_adapter_service.d.ts.map +1 -1
- package/dist/market_adapter/core/market_adapter_service.js +102 -98
- package/dist/market_adapter/core/market_adapter_service.js.map +1 -1
- package/dist/market_adapter/core/strategies/ama.d.ts +21 -1
- package/dist/market_adapter/core/strategies/ama.d.ts.map +1 -1
- package/dist/market_adapter/core/strategies/ama.js +10 -10
- package/dist/market_adapter/core/strategies/ama.js.map +1 -1
- package/dist/market_adapter/core/strategies/ama_slope_model.d.ts +7 -11
- package/dist/market_adapter/core/strategies/ama_slope_model.d.ts.map +1 -1
- package/dist/market_adapter/core/strategies/ama_slope_model.js +23 -30
- package/dist/market_adapter/core/strategies/ama_slope_model.js.map +1 -1
- package/dist/market_adapter/core/strategies/atr/calculator.d.ts +5 -4
- package/dist/market_adapter/core/strategies/atr/calculator.d.ts.map +1 -1
- package/dist/market_adapter/core/strategies/atr/calculator.js +5 -3
- package/dist/market_adapter/core/strategies/atr/calculator.js.map +1 -1
- package/dist/market_adapter/core/strategies/collateral_manager.d.ts +4 -7
- package/dist/market_adapter/core/strategies/collateral_manager.d.ts.map +1 -1
- package/dist/market_adapter/core/strategies/collateral_manager.js +5 -4
- package/dist/market_adapter/core/strategies/collateral_manager.js.map +1 -1
- package/dist/market_adapter/core/strategies/regime_gate.d.ts +11 -19
- package/dist/market_adapter/core/strategies/regime_gate.d.ts.map +1 -1
- package/dist/market_adapter/core/strategies/regime_gate.js +22 -22
- package/dist/market_adapter/core/strategies/regime_gate.js.map +1 -1
- package/dist/market_adapter/index.d.ts +24 -2
- package/dist/market_adapter/index.d.ts.map +1 -1
- package/dist/market_adapter/index.js +82 -56
- package/dist/market_adapter/index.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +26 -37
- package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts +4 -12
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.js +100 -66
- package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
- package/dist/market_adapter/inputs/kibana_source.d.ts +4 -9
- package/dist/market_adapter/inputs/kibana_source.d.ts.map +1 -1
- package/dist/market_adapter/inputs/kibana_source.js +17 -18
- package/dist/market_adapter/inputs/kibana_source.js.map +1 -1
- package/dist/market_adapter/interval_utils.d.ts +1 -4
- package/dist/market_adapter/interval_utils.d.ts.map +1 -1
- package/dist/market_adapter/interval_utils.js +2 -3
- package/dist/market_adapter/interval_utils.js.map +1 -1
- package/dist/market_adapter/log_format.d.ts +1 -9
- package/dist/market_adapter/log_format.d.ts.map +1 -1
- package/dist/market_adapter/log_format.js +7 -8
- package/dist/market_adapter/log_format.js.map +1 -1
- package/dist/market_adapter/lp_chart_core.d.ts +1 -4
- package/dist/market_adapter/lp_chart_core.d.ts.map +1 -1
- package/dist/market_adapter/lp_chart_core.js +22 -20
- package/dist/market_adapter/lp_chart_core.js.map +1 -1
- package/dist/market_adapter/lp_chart_runner.d.ts +13 -24
- package/dist/market_adapter/lp_chart_runner.d.ts.map +1 -1
- package/dist/market_adapter/lp_chart_runner.js +58 -56
- package/dist/market_adapter/lp_chart_runner.js.map +1 -1
- package/dist/market_adapter/lp_chart_strategy_loader.d.ts +4 -17
- package/dist/market_adapter/lp_chart_strategy_loader.d.ts.map +1 -1
- package/dist/market_adapter/lp_chart_strategy_loader.js +36 -36
- package/dist/market_adapter/lp_chart_strategy_loader.js.map +1 -1
- package/dist/market_adapter/market_adapter.d.ts +51 -72
- package/dist/market_adapter/market_adapter.d.ts.map +1 -1
- package/dist/market_adapter/market_adapter.js +227 -192
- package/dist/market_adapter/market_adapter.js.map +1 -1
- package/dist/market_adapter/merge_lp_data.js +17 -16
- package/dist/market_adapter/merge_lp_data.js.map +1 -1
- package/dist/market_adapter/test_helpers.d.ts +9 -13
- package/dist/market_adapter/test_helpers.d.ts.map +1 -1
- package/dist/market_adapter/test_helpers.js +14 -14
- package/dist/market_adapter/test_helpers.js.map +1 -1
- package/dist/market_adapter/utils/adapter_client.d.ts +6 -14
- package/dist/market_adapter/utils/adapter_client.d.ts.map +1 -1
- package/dist/market_adapter/utils/adapter_client.js +16 -15
- package/dist/market_adapter/utils/adapter_client.js.map +1 -1
- package/dist/market_adapter/utils/atomic_write.d.ts +2 -5
- package/dist/market_adapter/utils/atomic_write.d.ts.map +1 -1
- package/dist/market_adapter/utils/atomic_write.js +5 -5
- package/dist/market_adapter/utils/atomic_write.js.map +1 -1
- package/dist/market_adapter/utils/chain.d.ts +19 -21
- package/dist/market_adapter/utils/chain.d.ts.map +1 -1
- package/dist/market_adapter/utils/chain.js +15 -16
- package/dist/market_adapter/utils/chain.js.map +1 -1
- package/dist/market_adapter/utils/data_discovery.d.ts +1 -4
- package/dist/market_adapter/utils/data_discovery.d.ts.map +1 -1
- package/dist/market_adapter/utils/data_discovery.js +10 -10
- package/dist/market_adapter/utils/data_discovery.js.map +1 -1
- package/dist/market_adapter/utils/dynamic_grid_snapshot.d.ts +1 -6
- package/dist/market_adapter/utils/dynamic_grid_snapshot.d.ts.map +1 -1
- package/dist/market_adapter/utils/dynamic_grid_snapshot.js +14 -14
- package/dist/market_adapter/utils/dynamic_grid_snapshot.js.map +1 -1
- package/dist/market_adapter/utils/file_lock.d.ts +1 -7
- package/dist/market_adapter/utils/file_lock.d.ts.map +1 -1
- package/dist/market_adapter/utils/file_lock.js +21 -25
- package/dist/market_adapter/utils/file_lock.js.map +1 -1
- package/dist/market_adapter/utils/native_history.d.ts +12 -12
- package/dist/market_adapter/utils/native_history.d.ts.map +1 -1
- package/dist/market_adapter/utils/native_history.js +15 -15
- package/dist/market_adapter/utils/native_history.js.map +1 -1
- package/dist/market_adapter/utils/paths.d.ts +2 -4
- package/dist/market_adapter/utils/paths.d.ts.map +1 -1
- package/dist/market_adapter/utils/paths.js +6 -5
- package/dist/market_adapter/utils/paths.js.map +1 -1
- package/dist/modules/account_bots.d.ts +7 -8
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +92 -114
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/account_orders.d.ts +5 -9
- package/dist/modules/account_orders.d.ts.map +1 -1
- package/dist/modules/account_orders.js +69 -32
- package/dist/modules/account_orders.js.map +1 -1
- package/dist/modules/authority_resolver.d.ts +32 -1
- package/dist/modules/authority_resolver.d.ts.map +1 -1
- package/dist/modules/authority_resolver.js +18 -12
- package/dist/modules/authority_resolver.js.map +1 -1
- package/dist/modules/bitshares-native/chain_client.d.ts +3 -8
- package/dist/modules/bitshares-native/chain_client.d.ts.map +1 -1
- package/dist/modules/bitshares-native/chain_client.js +21 -16
- package/dist/modules/bitshares-native/chain_client.js.map +1 -1
- package/dist/modules/bitshares-native/crypto/ecc.browser.d.ts +21 -29
- package/dist/modules/bitshares-native/crypto/ecc.browser.d.ts.map +1 -1
- package/dist/modules/bitshares-native/crypto/ecc.browser.js +72 -38
- package/dist/modules/bitshares-native/crypto/ecc.browser.js.map +1 -1
- package/dist/modules/bitshares-native/crypto/ecc.d.ts +9 -36
- package/dist/modules/bitshares-native/crypto/ecc.d.ts.map +1 -1
- package/dist/modules/bitshares-native/crypto/ecc.js +40 -83
- package/dist/modules/bitshares-native/crypto/ecc.js.map +1 -1
- package/dist/modules/bitshares-native/crypto/ecc_selector.d.ts +14 -1
- package/dist/modules/bitshares-native/crypto/ecc_selector.d.ts.map +1 -1
- package/dist/modules/bitshares-native/crypto/ecc_selector.js +5 -3
- package/dist/modules/bitshares-native/crypto/ecc_selector.js.map +1 -1
- package/dist/modules/bitshares-native/index.d.ts +10 -2
- package/dist/modules/bitshares-native/index.d.ts.map +1 -1
- package/dist/modules/bitshares-native/index.js +68 -31
- package/dist/modules/bitshares-native/index.js.map +1 -1
- package/dist/modules/bitshares-native/lru_cache.d.ts +1 -4
- package/dist/modules/bitshares-native/lru_cache.d.ts.map +1 -1
- package/dist/modules/bitshares-native/lru_cache.js +6 -3
- package/dist/modules/bitshares-native/lru_cache.js.map +1 -1
- package/dist/modules/bitshares-native/resolvers.d.ts +3 -6
- package/dist/modules/bitshares-native/resolvers.d.ts.map +1 -1
- package/dist/modules/bitshares-native/resolvers.js +11 -9
- package/dist/modules/bitshares-native/resolvers.js.map +1 -1
- package/dist/modules/bitshares-native/serial/chain_constants.d.ts +53 -55
- package/dist/modules/bitshares-native/serial/chain_constants.d.ts.map +1 -1
- package/dist/modules/bitshares-native/serial/chain_constants.js +28 -27
- package/dist/modules/bitshares-native/serial/chain_constants.js.map +1 -1
- package/dist/modules/bitshares-native/serial/index.d.ts +5 -9
- package/dist/modules/bitshares-native/serial/index.d.ts.map +1 -1
- package/dist/modules/bitshares-native/serial/index.js +46 -12
- package/dist/modules/bitshares-native/serial/index.js.map +1 -1
- package/dist/modules/bitshares-native/serial/operations.d.ts +102 -100
- package/dist/modules/bitshares-native/serial/operations.d.ts.map +1 -1
- package/dist/modules/bitshares-native/serial/operations.js +473 -471
- package/dist/modules/bitshares-native/serial/operations.js.map +1 -1
- package/dist/modules/bitshares-native/serial/serializer.d.ts +2 -7
- package/dist/modules/bitshares-native/serial/serializer.d.ts.map +1 -1
- package/dist/modules/bitshares-native/serial/serializer.js +8 -3
- package/dist/modules/bitshares-native/serial/serializer.js.map +1 -1
- package/dist/modules/bitshares-native/serial/types.d.ts +26 -41
- package/dist/modules/bitshares-native/serial/types.d.ts.map +1 -1
- package/dist/modules/bitshares-native/serial/types.js +76 -42
- package/dist/modules/bitshares-native/serial/types.js.map +1 -1
- package/dist/modules/bitshares-native/signing_client.d.ts +1 -5
- package/dist/modules/bitshares-native/signing_client.d.ts.map +1 -1
- package/dist/modules/bitshares-native/signing_client.js +49 -11
- package/dist/modules/bitshares-native/signing_client.js.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.d.ts +1 -4
- package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.js +47 -84
- package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
- package/dist/modules/bitshares-native/transport.d.ts +1 -8
- package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
- package/dist/modules/bitshares-native/transport.js +22 -15
- package/dist/modules/bitshares-native/transport.js.map +1 -1
- package/dist/modules/bitshares-native/tx/builder.d.ts +3 -8
- package/dist/modules/bitshares-native/tx/builder.d.ts.map +1 -1
- package/dist/modules/bitshares-native/tx/builder.js +61 -23
- package/dist/modules/bitshares-native/tx/builder.js.map +1 -1
- package/dist/modules/bitshares-native/tx/tx_cache.d.ts +1 -8
- package/dist/modules/bitshares-native/tx/tx_cache.d.ts.map +1 -1
- package/dist/modules/bitshares-native/tx/tx_cache.js +21 -17
- package/dist/modules/bitshares-native/tx/tx_cache.js.map +1 -1
- package/dist/modules/bitshares_client.d.ts +12 -23
- package/dist/modules/bitshares_client.d.ts.map +1 -1
- package/dist/modules/bitshares_client.js +109 -56
- package/dist/modules/bitshares_client.js.map +1 -1
- package/dist/modules/bot_settings.d.ts +1 -13
- package/dist/modules/bot_settings.d.ts.map +1 -1
- package/dist/modules/bot_settings.js +34 -34
- package/dist/modules/bot_settings.js.map +1 -1
- package/dist/modules/bots_file_lock.d.ts +3 -9
- package/dist/modules/bots_file_lock.d.ts.map +1 -1
- package/dist/modules/bots_file_lock.js +8 -9
- package/dist/modules/bots_file_lock.js.map +1 -1
- package/dist/modules/chain_keys.d.ts +7 -39
- package/dist/modules/chain_keys.d.ts.map +1 -1
- package/dist/modules/chain_keys.js +126 -90
- package/dist/modules/chain_keys.js.map +1 -1
- package/dist/modules/chain_orders.d.ts +46 -52
- package/dist/modules/chain_orders.d.ts.map +1 -1
- package/dist/modules/chain_orders.js +152 -104
- package/dist/modules/chain_orders.js.map +1 -1
- package/dist/modules/config.d.ts +50 -57
- package/dist/modules/config.d.ts.map +1 -1
- package/dist/modules/config.js +28 -24
- package/dist/modules/config.js.map +1 -1
- package/dist/modules/constants.d.ts +492 -492
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +79 -39
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/cr_planner.d.ts +1 -13
- package/dist/modules/cr_planner.d.ts.map +1 -1
- package/dist/modules/cr_planner.js +28 -28
- package/dist/modules/cr_planner.js.map +1 -1
- package/dist/modules/credential_policy.d.ts +18 -33
- package/dist/modules/credential_policy.d.ts.map +1 -1
- package/dist/modules/credential_policy.js +114 -87
- package/dist/modules/credential_policy.js.map +1 -1
- package/dist/modules/credential_runtime.d.ts +11 -21
- package/dist/modules/credential_runtime.d.ts.map +1 -1
- package/dist/modules/credential_runtime.js +40 -40
- package/dist/modules/credential_runtime.js.map +1 -1
- package/dist/modules/credential_session_cache.d.ts +8 -8
- package/dist/modules/credential_session_cache.d.ts.map +1 -1
- package/dist/modules/credential_session_cache.js +41 -8
- package/dist/modules/credential_session_cache.js.map +1 -1
- package/dist/modules/credit_runtime.d.ts +51 -316
- package/dist/modules/credit_runtime.d.ts.map +1 -1
- package/dist/modules/credit_runtime.js +257 -183
- package/dist/modules/credit_runtime.js.map +1 -1
- package/dist/modules/crypto/browser_provider.js +6 -6
- package/dist/modules/crypto/browser_provider.js.map +1 -1
- package/dist/modules/crypto/index.d.ts.map +1 -1
- package/dist/modules/crypto/index.js +2 -0
- package/dist/modules/crypto/index.js.map +1 -1
- package/dist/modules/crypto/sync.js +3 -3
- package/dist/modules/crypto/sync.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +124 -73
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +198 -3577
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +322 -0
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -0
- package/dist/modules/dexbot_cow_runtime.js +1836 -0
- package/dist/modules/dexbot_cow_runtime.js.map +1 -0
- package/dist/modules/dexbot_credential_client.d.ts +5 -11
- package/dist/modules/dexbot_credential_client.d.ts.map +1 -1
- package/dist/modules/dexbot_credential_client.js +32 -30
- package/dist/modules/dexbot_credential_client.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +47 -15
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +565 -22
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +79 -10
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +294 -55
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts +26 -0
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -0
- package/dist/modules/dexbot_startup_runtime.js +575 -0
- package/dist/modules/dexbot_startup_runtime.js.map +1 -0
- package/dist/modules/dexbot_state_recovery.d.ts +115 -0
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -0
- package/dist/modules/dexbot_state_recovery.js +431 -0
- package/dist/modules/dexbot_state_recovery.js.map +1 -0
- package/dist/modules/fund_registry.d.ts +1 -13
- package/dist/modules/fund_registry.d.ts.map +1 -1
- package/dist/modules/fund_registry.js +23 -22
- package/dist/modules/fund_registry.js.map +1 -1
- package/dist/modules/general_settings.d.ts +3 -7
- package/dist/modules/general_settings.d.ts.map +1 -1
- package/dist/modules/general_settings.js +15 -15
- package/dist/modules/general_settings.js.map +1 -1
- package/dist/modules/graceful_shutdown.d.ts +1 -6
- package/dist/modules/graceful_shutdown.d.ts.map +1 -1
- package/dist/modules/graceful_shutdown.js +19 -16
- package/dist/modules/graceful_shutdown.js.map +1 -1
- package/dist/modules/key_store.d.ts.map +1 -1
- package/dist/modules/key_store.js +53 -20
- package/dist/modules/key_store.js.map +1 -1
- package/dist/modules/launcher/bot_supervisor.d.ts +23 -66
- package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
- package/dist/modules/launcher/bot_supervisor.js +115 -107
- package/dist/modules/launcher/bot_supervisor.js.map +1 -1
- package/dist/modules/launcher/child_env.d.ts +2 -5
- package/dist/modules/launcher/child_env.d.ts.map +1 -1
- package/dist/modules/launcher/child_env.js +4 -4
- package/dist/modules/launcher/child_env.js.map +1 -1
- package/dist/modules/launcher/credential_bootstrap.d.ts +4 -8
- package/dist/modules/launcher/credential_bootstrap.d.ts.map +1 -1
- package/dist/modules/launcher/credential_bootstrap.js +30 -26
- package/dist/modules/launcher/credential_bootstrap.js.map +1 -1
- package/dist/modules/launcher/credential_daemon.d.ts +8 -15
- package/dist/modules/launcher/credential_daemon.d.ts.map +1 -1
- package/dist/modules/launcher/credential_daemon.js +72 -41
- package/dist/modules/launcher/credential_daemon.js.map +1 -1
- package/dist/modules/launcher/credential_secret.d.ts +1 -4
- package/dist/modules/launcher/credential_secret.d.ts.map +1 -1
- package/dist/modules/launcher/credential_secret.js +36 -4
- package/dist/modules/launcher/credential_secret.js.map +1 -1
- package/dist/modules/launcher/foreign_cred_daemon.d.ts +32 -1
- package/dist/modules/launcher/foreign_cred_daemon.d.ts.map +1 -1
- package/dist/modules/launcher/foreign_cred_daemon.js +25 -28
- package/dist/modules/launcher/foreign_cred_daemon.js.map +1 -1
- package/dist/modules/launcher/headless_password.d.ts +1 -4
- package/dist/modules/launcher/headless_password.d.ts.map +1 -1
- package/dist/modules/launcher/headless_password.js +11 -11
- package/dist/modules/launcher/headless_password.js.map +1 -1
- package/dist/modules/launcher/launch_modes.d.ts +15 -14
- package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
- package/dist/modules/launcher/launch_modes.js +8 -8
- package/dist/modules/launcher/launch_modes.js.map +1 -1
- package/dist/modules/launcher/market_adapter_runtime.d.ts +10 -19
- package/dist/modules/launcher/market_adapter_runtime.d.ts.map +1 -1
- package/dist/modules/launcher/market_adapter_runtime.js +33 -34
- package/dist/modules/launcher/market_adapter_runtime.js.map +1 -1
- package/dist/modules/launcher/market_adapter_watchdog.d.ts +10 -17
- package/dist/modules/launcher/market_adapter_watchdog.d.ts.map +1 -1
- package/dist/modules/launcher/market_adapter_watchdog.js +42 -39
- package/dist/modules/launcher/market_adapter_watchdog.js.map +1 -1
- package/dist/modules/launcher/monolithic_runtime.d.ts +28 -57
- package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
- package/dist/modules/launcher/monolithic_runtime.js +135 -108
- package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
- package/dist/modules/launcher/runtime_entry.d.ts +5 -10
- package/dist/modules/launcher/runtime_entry.d.ts.map +1 -1
- package/dist/modules/launcher/runtime_entry.js +14 -14
- package/dist/modules/launcher/runtime_entry.js.map +1 -1
- package/dist/modules/launcher/status_reporting.d.ts +17 -37
- package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
- package/dist/modules/launcher/status_reporting.js +35 -34
- package/dist/modules/launcher/status_reporting.js.map +1 -1
- package/dist/modules/launcher/supervisor_control.d.ts +1 -4
- package/dist/modules/launcher/supervisor_control.d.ts.map +1 -1
- package/dist/modules/launcher/supervisor_control.js +9 -4
- package/dist/modules/launcher/supervisor_control.js.map +1 -1
- package/dist/modules/logger.d.ts +3 -1
- package/dist/modules/logger.d.ts.map +1 -1
- package/dist/modules/logger.js +38 -28
- package/dist/modules/logger.js.map +1 -1
- package/dist/modules/market_adapter_whitelist.d.ts +2 -10
- package/dist/modules/market_adapter_whitelist.d.ts.map +1 -1
- package/dist/modules/market_adapter_whitelist.js +18 -16
- package/dist/modules/market_adapter_whitelist.js.map +1 -1
- package/dist/modules/node_health_cache.d.ts +2 -12
- package/dist/modules/node_health_cache.d.ts.map +1 -1
- package/dist/modules/node_health_cache.js +24 -24
- package/dist/modules/node_health_cache.js.map +1 -1
- package/dist/modules/node_manager.d.ts +1 -1
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +44 -38
- package/dist/modules/node_manager.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +12 -12
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +111 -76
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/async_lock.d.ts +1 -2
- package/dist/modules/order/async_lock.d.ts.map +1 -1
- package/dist/modules/order/async_lock.js +4 -4
- package/dist/modules/order/async_lock.js.map +1 -1
- package/dist/modules/order/export.d.ts +8 -14
- package/dist/modules/order/export.d.ts.map +1 -1
- package/dist/modules/order/export.js +77 -42
- package/dist/modules/order/export.js.map +1 -1
- package/dist/modules/order/format.d.ts +17 -22
- package/dist/modules/order/format.d.ts.map +1 -1
- package/dist/modules/order/format.js +25 -31
- package/dist/modules/order/format.js.map +1 -1
- package/dist/modules/order/grid.d.ts +18 -104
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +246 -188
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts +8 -8
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +24 -24
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts +224 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +145 -72
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/index.d.ts +1 -1
- package/dist/modules/order/index.d.ts.map +1 -1
- package/dist/modules/order/index.js +42 -7
- package/dist/modules/order/index.js.map +1 -1
- package/dist/modules/order/logger.d.ts +6 -16
- package/dist/modules/order/logger.d.ts.map +1 -1
- package/dist/modules/order/logger.js +76 -53
- package/dist/modules/order/logger.js.map +1 -1
- package/dist/modules/order/logger_state.d.ts +4 -4
- package/dist/modules/order/logger_state.d.ts.map +1 -1
- package/dist/modules/order/logger_state.js +3 -1
- package/dist/modules/order/logger_state.js.map +1 -1
- package/dist/modules/order/manager.d.ts +160 -37
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +166 -128
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/processed_fill_store.d.ts +6 -10
- package/dist/modules/order/processed_fill_store.d.ts.map +1 -1
- package/dist/modules/order/processed_fill_store.js +10 -9
- package/dist/modules/order/processed_fill_store.js.map +1 -1
- package/dist/modules/order/strategy.d.ts +3 -3
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +29 -27
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts +9 -19
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +176 -126
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +55 -85
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +155 -88
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +27 -72
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +144 -105
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +74 -44
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +188 -149
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/validate.d.ts +21 -43
- package/dist/modules/order/utils/validate.d.ts.map +1 -1
- package/dist/modules/order/utils/validate.js +140 -110
- package/dist/modules/order/utils/validate.js.map +1 -1
- package/dist/modules/order/working_grid.d.ts +1 -4
- package/dist/modules/order/working_grid.d.ts.map +1 -1
- package/dist/modules/order/working_grid.js +9 -7
- package/dist/modules/order/working_grid.js.map +1 -1
- package/dist/modules/path_api.js +2 -2
- package/dist/modules/path_api.js.map +1 -1
- package/dist/modules/paths.d.ts +51 -55
- package/dist/modules/paths.d.ts.map +1 -1
- package/dist/modules/paths.js +72 -52
- package/dist/modules/paths.js.map +1 -1
- package/dist/modules/process_discovery.d.ts +1 -1
- package/dist/modules/process_discovery.d.ts.map +1 -1
- package/dist/modules/process_discovery.js +7 -7
- package/dist/modules/process_discovery.js.map +1 -1
- package/dist/modules/runtime.d.ts +1 -1
- package/dist/modules/runtime.d.ts.map +1 -1
- package/dist/modules/runtime.js +4 -4
- package/dist/modules/runtime.js.map +1 -1
- package/dist/modules/runtime_settings.d.ts +1 -0
- package/dist/modules/runtime_settings.d.ts.map +1 -1
- package/dist/modules/runtime_settings.js +15 -10
- package/dist/modules/runtime_settings.js.map +1 -1
- package/dist/modules/settings_merge.d.ts +1 -4
- package/dist/modules/settings_merge.d.ts.map +1 -1
- package/dist/modules/settings_merge.js +2 -1
- package/dist/modules/settings_merge.js.map +1 -1
- package/dist/modules/storage/browser_adapter.d.ts +7 -3
- package/dist/modules/storage/browser_adapter.d.ts.map +1 -1
- package/dist/modules/storage/browser_adapter.js +2 -0
- package/dist/modules/storage/browser_adapter.js.map +1 -1
- package/dist/modules/storage/index.d.ts +2 -2
- package/dist/modules/storage/index.d.ts.map +1 -1
- package/dist/modules/storage/index.js +2 -5
- package/dist/modules/storage/index.js.map +1 -1
- package/dist/modules/storage/node_adapter.d.ts +3 -3
- package/dist/modules/storage/node_adapter.d.ts.map +1 -1
- package/dist/modules/storage/node_adapter.js +9 -7
- package/dist/modules/storage/node_adapter.js.map +1 -1
- package/dist/modules/storage/types.d.ts +13 -10
- package/dist/modules/storage/types.d.ts.map +1 -1
- package/dist/modules/types.d.ts +0 -2
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/utils/base58check.d.ts +1 -9
- package/dist/modules/utils/base58check.d.ts.map +1 -1
- package/dist/modules/utils/base58check.js +14 -16
- package/dist/modules/utils/base58check.js.map +1 -1
- package/dist/modules/utils/build_dir.d.ts +2 -5
- package/dist/modules/utils/build_dir.d.ts.map +1 -1
- package/dist/modules/utils/build_dir.js +6 -3
- package/dist/modules/utils/build_dir.js.map +1 -1
- package/dist/modules/utils/errors.d.ts +2 -0
- package/dist/modules/utils/errors.d.ts.map +1 -0
- package/dist/modules/utils/errors.js +12 -0
- package/dist/modules/utils/errors.js.map +1 -0
- package/dist/modules/utils/fs_utils.d.ts +1 -8
- package/dist/modules/utils/fs_utils.d.ts.map +1 -1
- package/dist/modules/utils/fs_utils.js +8 -3
- package/dist/modules/utils/fs_utils.js.map +1 -1
- package/dist/modules/utils/math_utils.d.ts +2 -6
- package/dist/modules/utils/math_utils.d.ts.map +1 -1
- package/dist/modules/utils/math_utils.js +6 -2
- package/dist/modules/utils/math_utils.js.map +1 -1
- package/dist/modules/validate_profiles.d.ts +1 -11
- package/dist/modules/validate_profiles.d.ts.map +1 -1
- package/dist/modules/validate_profiles.js +25 -27
- package/dist/modules/validate_profiles.js.map +1 -1
- package/dist/pm2.d.ts +7 -6
- package/dist/pm2.d.ts.map +1 -1
- package/dist/pm2.js +132 -106
- package/dist/pm2.js.map +1 -1
- package/dist/scripts/analyze-git.d.ts.map +1 -1
- package/dist/scripts/analyze-git.js +15 -11
- package/dist/scripts/analyze-git.js.map +1 -1
- package/dist/scripts/analyze-orders.d.ts +146 -1
- package/dist/scripts/analyze-orders.d.ts.map +1 -1
- package/dist/scripts/analyze-orders.js +81 -105
- package/dist/scripts/analyze-orders.js.map +1 -1
- package/dist/scripts/diagnose-pool-history.d.ts.map +1 -1
- package/dist/scripts/diagnose-pool-history.js +9 -8
- package/dist/scripts/diagnose-pool-history.js.map +1 -1
- package/dist/scripts/divergence-calc.d.ts.map +1 -1
- package/dist/scripts/divergence-calc.js +11 -8
- package/dist/scripts/divergence-calc.js.map +1 -1
- package/dist/scripts/fix-err-message.d.ts +3 -0
- package/dist/scripts/fix-err-message.d.ts.map +1 -0
- package/dist/scripts/fix-err-message.js +198 -0
- package/dist/scripts/fix-err-message.js.map +1 -0
- package/dist/scripts/generate_lp_chart.d.ts +2 -9
- package/dist/scripts/generate_lp_chart.d.ts.map +1 -1
- package/dist/scripts/generate_lp_chart.js +14 -10
- package/dist/scripts/generate_lp_chart.js.map +1 -1
- package/dist/scripts/generate_mainnet_corpus_report.js +4 -4
- package/dist/scripts/generate_mainnet_corpus_report.js.map +1 -1
- package/dist/scripts/generate_market_adapter_whitelist.d.ts +17 -1
- package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +1 -1
- package/dist/scripts/generate_market_adapter_whitelist.js +23 -20
- package/dist/scripts/generate_market_adapter_whitelist.js.map +1 -1
- package/dist/scripts/migrate_bot_keys.d.ts +1 -4
- package/dist/scripts/migrate_bot_keys.d.ts.map +1 -1
- package/dist/scripts/migrate_bot_keys.js +48 -124
- package/dist/scripts/migrate_bot_keys.js.map +1 -1
- package/dist/scripts/native_release_gates.d.ts.map +1 -1
- package/dist/scripts/native_release_gates.js +18 -14
- package/dist/scripts/native_release_gates.js.map +1 -1
- package/dist/scripts/print_grid.js +1 -1
- package/dist/scripts/print_grid.js.map +1 -1
- package/dist/scripts/run-tests.js +1 -0
- package/dist/scripts/run-tests.js.map +1 -1
- package/dist/scripts/runner.d.ts +1 -11
- package/dist/scripts/runner.d.ts.map +1 -1
- package/dist/scripts/runner.js +21 -19
- package/dist/scripts/runner.js.map +1 -1
- package/dist/scripts/test-credit-renewal.d.ts.map +1 -1
- package/dist/scripts/test-credit-renewal.js +2 -1
- package/dist/scripts/test-credit-renewal.js.map +1 -1
- package/dist/scripts/update.d.ts.map +1 -1
- package/dist/scripts/update.js +85 -79
- package/dist/scripts/update.js.map +1 -1
- package/dist/scripts/validate_bots.js +2 -2
- package/dist/scripts/validate_bots.js.map +1 -1
- package/dist/scripts/verify-browser-bundle.js +25 -1
- package/dist/scripts/verify-browser-bundle.js.map +1 -1
- package/dist/unlock.d.ts +4 -17
- package/dist/unlock.d.ts.map +1 -1
- package/dist/unlock.js +141 -108
- package/dist/unlock.js.map +1 -1
- package/package.json +7 -2
- package/dist/analysis/ama_fitting/analyze_ama_price_changes.d.ts +0 -3
- package/dist/analysis/ama_fitting/analyze_ama_price_changes.d.ts.map +0 -1
- package/dist/analysis/ama_fitting/analyze_ama_price_changes.js +0 -149
- package/dist/analysis/ama_fitting/analyze_ama_price_changes.js.map +0 -1
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts +0 -3
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts.map +0 -1
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js +0 -387
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js.map +0 -1
- package/dist/analysis/ama_fitting/calibrate_convergence_er.d.ts +0 -3
- package/dist/analysis/ama_fitting/calibrate_convergence_er.d.ts.map +0 -1
- package/dist/analysis/ama_fitting/calibrate_convergence_er.js +0 -200
- package/dist/analysis/ama_fitting/calibrate_convergence_er.js.map +0 -1
- package/dist/analysis/ama_fitting/fetch_lp_candles.d.ts +0 -3
- package/dist/analysis/ama_fitting/fetch_lp_candles.d.ts.map +0 -1
- package/dist/analysis/ama_fitting/fetch_lp_candles.js +0 -212
- package/dist/analysis/ama_fitting/fetch_lp_candles.js.map +0 -1
- package/dist/analysis/ama_fitting/generate_unified_comparison_chart.d.ts +0 -31
- package/dist/analysis/ama_fitting/generate_unified_comparison_chart.d.ts.map +0 -1
- package/dist/analysis/ama_fitting/generate_unified_comparison_chart.js +0 -249
- package/dist/analysis/ama_fitting/generate_unified_comparison_chart.js.map +0 -1
- package/dist/analysis/ama_fitting/optimizer_high_resolution.d.ts +0 -2
- package/dist/analysis/ama_fitting/optimizer_high_resolution.d.ts.map +0 -1
- package/dist/analysis/ama_fitting/optimizer_high_resolution.js +0 -782
- package/dist/analysis/ama_fitting/optimizer_high_resolution.js.map +0 -1
- package/dist/analysis/analyze_derivatives.d.ts +0 -95
- package/dist/analysis/analyze_derivatives.d.ts.map +0 -1
- package/dist/analysis/analyze_derivatives.js +0 -295
- package/dist/analysis/analyze_derivatives.js.map +0 -1
- package/dist/analysis/analyze_dynamic_weight.d.ts +0 -14
- package/dist/analysis/analyze_dynamic_weight.d.ts.map +0 -1
- package/dist/analysis/analyze_dynamic_weight.js +0 -207
- package/dist/analysis/analyze_dynamic_weight.js.map +0 -1
- package/dist/analysis/analyze_kalman.d.ts +0 -13
- package/dist/analysis/analyze_kalman.d.ts.map +0 -1
- package/dist/analysis/analyze_kalman.js +0 -123
- package/dist/analysis/analyze_kalman.js.map +0 -1
- package/dist/analysis/analyze_regime.d.ts +0 -20
- package/dist/analysis/analyze_regime.d.ts.map +0 -1
- package/dist/analysis/analyze_regime.js +0 -139
- package/dist/analysis/analyze_regime.js.map +0 -1
- package/dist/analysis/analyze_regime_windows.d.ts +0 -19
- package/dist/analysis/analyze_regime_windows.d.ts.map +0 -1
- package/dist/analysis/analyze_regime_windows.js +0 -413
- package/dist/analysis/analyze_regime_windows.js.map +0 -1
- package/dist/analysis/analyze_risk_profile.d.ts +0 -3
- package/dist/analysis/analyze_risk_profile.d.ts.map +0 -1
- package/dist/analysis/analyze_risk_profile.js +0 -159
- package/dist/analysis/analyze_risk_profile.js.map +0 -1
- package/dist/analysis/analyze_trade_heatmap.d.ts +0 -3
- package/dist/analysis/analyze_trade_heatmap.d.ts.map +0 -1
- package/dist/analysis/analyze_trade_heatmap.js +0 -367
- package/dist/analysis/analyze_trade_heatmap.js.map +0 -1
- package/dist/analysis/analyze_volatility.d.ts +0 -22
- package/dist/analysis/analyze_volatility.d.ts.map +0 -1
- package/dist/analysis/analyze_volatility.js +0 -172
- package/dist/analysis/analyze_volatility.js.map +0 -1
- package/dist/analysis/bot_fitting/backtest_ama_sweep.d.ts +0 -99
- package/dist/analysis/bot_fitting/backtest_ama_sweep.d.ts.map +0 -1
- package/dist/analysis/bot_fitting/backtest_ama_sweep.js +0 -804
- package/dist/analysis/bot_fitting/backtest_ama_sweep.js.map +0 -1
- package/dist/analysis/bot_fitting/backtest_bot_fitting.d.ts +0 -2
- package/dist/analysis/bot_fitting/backtest_bot_fitting.d.ts.map +0 -1
- package/dist/analysis/bot_fitting/backtest_bot_fitting.js +0 -347
- package/dist/analysis/bot_fitting/backtest_bot_fitting.js.map +0 -1
- package/dist/analysis/bot_fitting/shared_utils.d.ts +0 -15
- package/dist/analysis/bot_fitting/shared_utils.d.ts.map +0 -1
- package/dist/analysis/bot_fitting/shared_utils.js +0 -48
- package/dist/analysis/bot_fitting/shared_utils.js.map +0 -1
- package/dist/analysis/bot_key_utils.d.ts +0 -22
- package/dist/analysis/bot_key_utils.d.ts.map +0 -1
- package/dist/analysis/bot_key_utils.js +0 -162
- package/dist/analysis/bot_key_utils.js.map +0 -1
- package/dist/analysis/bot_usage/discover_bot_accounts.d.ts +0 -3
- package/dist/analysis/bot_usage/discover_bot_accounts.d.ts.map +0 -1
- package/dist/analysis/bot_usage/discover_bot_accounts.js +0 -498
- package/dist/analysis/bot_usage/discover_bot_accounts.js.map +0 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +0 -546
- package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +0 -1
- package/dist/analysis/bot_usage/kibana_bot_queries.js +0 -427
- package/dist/analysis/bot_usage/kibana_bot_queries.js.map +0 -1
- package/dist/analysis/derivative_chart_generator.d.ts +0 -18
- package/dist/analysis/derivative_chart_generator.d.ts.map +0 -1
- package/dist/analysis/derivative_chart_generator.js +0 -896
- package/dist/analysis/derivative_chart_generator.js.map +0 -1
- package/dist/analysis/price_sources.d.ts +0 -34
- package/dist/analysis/price_sources.d.ts.map +0 -1
- package/dist/analysis/price_sources.js +0 -103
- package/dist/analysis/price_sources.js.map +0 -1
- package/dist/analysis/trade_profitability.d.ts +0 -115
- package/dist/analysis/trade_profitability.d.ts.map +0 -1
- package/dist/analysis/trade_profitability.js +0 -1247
- package/dist/analysis/trade_profitability.js.map +0 -1
- package/dist/analysis/tradingview/analyze_tradingview.d.ts +0 -36
- package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +0 -1
- package/dist/analysis/tradingview/analyze_tradingview.js +0 -172
- package/dist/analysis/tradingview/analyze_tradingview.js.map +0 -1
- package/dist/analysis/trend_detection/derivative_analyzer.d.ts +0 -250
- package/dist/analysis/trend_detection/derivative_analyzer.d.ts.map +0 -1
- package/dist/analysis/trend_detection/derivative_analyzer.js +0 -903
- package/dist/analysis/trend_detection/derivative_analyzer.js.map +0 -1
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts +0 -6
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +0 -1
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +0 -1450
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +0 -1
- package/dist/analysis/trend_detection/kalman_chart_generator.d.ts +0 -6
- package/dist/analysis/trend_detection/kalman_chart_generator.d.ts.map +0 -1
- package/dist/analysis/trend_detection/kalman_chart_generator.js +0 -408
- package/dist/analysis/trend_detection/kalman_chart_generator.js.map +0 -1
- package/dist/analysis/trend_detection/regime_chart_generator.d.ts +0 -6
- package/dist/analysis/trend_detection/regime_chart_generator.d.ts.map +0 -1
- package/dist/analysis/trend_detection/regime_chart_generator.js +0 -345
- package/dist/analysis/trend_detection/regime_chart_generator.js.map +0 -1
- package/dist/analysis/trend_detection/tests/test_kalman_trend.d.ts +0 -2
- package/dist/analysis/trend_detection/tests/test_kalman_trend.d.ts.map +0 -1
- package/dist/analysis/trend_detection/tests/test_kalman_trend.js +0 -130
- package/dist/analysis/trend_detection/tests/test_kalman_trend.js.map +0 -1
- package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.d.ts +0 -2
- package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.d.ts.map +0 -1
- package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.js +0 -38
- package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.js.map +0 -1
- package/dist/analysis/trend_detection/volatility_chart_generator.d.ts +0 -6
- package/dist/analysis/trend_detection/volatility_chart_generator.d.ts.map +0 -1
- package/dist/analysis/trend_detection/volatility_chart_generator.js +0 -732
- package/dist/analysis/trend_detection/volatility_chart_generator.js.map +0 -1
|
@@ -53,35 +53,66 @@
|
|
|
53
53
|
*
|
|
54
54
|
* ===============================================================================
|
|
55
55
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
56
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
57
|
+
if (k2 === undefined) k2 = k;
|
|
58
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
59
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
60
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
61
|
+
}
|
|
62
|
+
Object.defineProperty(o, k2, desc);
|
|
63
|
+
}) : (function(o, m, k, k2) {
|
|
64
|
+
if (k2 === undefined) k2 = k;
|
|
65
|
+
o[k2] = m[k];
|
|
66
|
+
}));
|
|
67
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
68
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
69
|
+
}) : function(o, v) {
|
|
70
|
+
o["default"] = v;
|
|
71
|
+
});
|
|
72
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
73
|
+
var ownKeys = function(o) {
|
|
74
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
75
|
+
var ar = [];
|
|
76
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
77
|
+
return ar;
|
|
78
|
+
};
|
|
79
|
+
return ownKeys(o);
|
|
80
|
+
};
|
|
81
|
+
return function (mod) {
|
|
82
|
+
if (mod && mod.__esModule) return mod;
|
|
83
|
+
var result = {};
|
|
84
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
85
|
+
__setModuleDefault(result, mod);
|
|
86
|
+
return result;
|
|
87
|
+
};
|
|
88
|
+
})();
|
|
89
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
90
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
91
|
+
};
|
|
92
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
93
|
+
const client = __importStar(require("./bitshares_client"));
|
|
94
|
+
const { BitShares } = client;
|
|
95
|
+
const chainKeys = __importStar(require("./chain_keys"));
|
|
96
|
+
const chainOrders = __importStar(require("./chain_orders"));
|
|
97
|
+
const fundRegistry = __importStar(require("./fund_registry"));
|
|
98
|
+
const DexbotFillRuntime = __importStar(require("./dexbot_fill_runtime"));
|
|
99
|
+
const dexbot_maintenance_runtime_1 = __importDefault(require("./dexbot_maintenance_runtime"));
|
|
100
|
+
const DexbotStateRecovery = __importStar(require("./dexbot_state_recovery"));
|
|
101
|
+
const DexbotStartupRuntime = __importStar(require("./dexbot_startup_runtime"));
|
|
102
|
+
const credit_runtime_1 = __importDefault(require("./credit_runtime"));
|
|
103
|
+
const paths_1 = require("./paths");
|
|
104
|
+
const Format = __importStar(require("./order/format"));
|
|
105
|
+
const dexbot_cow_runtime_1 = __importDefault(require("./dexbot_cow_runtime"));
|
|
106
|
+
const processed_fill_store_1 = require("./order/processed_fill_store");
|
|
107
|
+
const constants_1 = require("./constants");
|
|
108
|
+
const bot_settings_1 = require("./bot_settings");
|
|
109
|
+
const errors_1 = require("./utils/errors");
|
|
110
|
+
function waitForConnected(...args) { return require('./bitshares_client').waitForConnected(...args); }
|
|
111
|
+
function getKeyStore(...args) { return require('./key_store').getKeyStore(...args); }
|
|
112
|
+
function hasExecutableActions(...args) { return require('./order/utils/validate').hasExecutableActions(...args); }
|
|
113
|
+
function getRecalculateTriggerFile(...args) { return require('./paths').getRecalculateTriggerFile(...args); }
|
|
114
|
+
function cloneWeightDistribution(...args) { return require('./order/utils/math').cloneWeightDistribution(...args); }
|
|
115
|
+
function resolveBotRuntimeSettings(...args) { return require('./runtime_settings').resolveBotRuntimeSettings(...args); }
|
|
85
116
|
class DEXBot {
|
|
86
117
|
config;
|
|
87
118
|
_baseWeightDistribution;
|
|
@@ -138,8 +169,7 @@ class DEXBot {
|
|
|
138
169
|
_reconnectUnregister;
|
|
139
170
|
_credentialRecoveryDeferredTimer;
|
|
140
171
|
_structuralGridResyncTimer;
|
|
141
|
-
_structuralGridResyncRunning;
|
|
142
|
-
_ghostOrderCancelAttempted;
|
|
172
|
+
_structuralGridResyncRunning = false;
|
|
143
173
|
_dustHealthCheckTimer;
|
|
144
174
|
_lastBroadcastHeartbeatAt;
|
|
145
175
|
_lastDeferredDustCount;
|
|
@@ -171,9 +201,9 @@ class DEXBot {
|
|
|
171
201
|
this.config.logging = rs.logging;
|
|
172
202
|
this._fillDedupeWindowMs = this.config.timing.FILL_DEDUPE_WINDOW_MS;
|
|
173
203
|
this._fillRecordRetentionMs = this.config.timing.FILL_RECORD_RETENTION_MS;
|
|
174
|
-
this._processedFillPersistBatchMs = TIMING.PROCESSED_FILL_PERSIST_BATCH_MS;
|
|
175
|
-
this._processedFillPersistBatchSize = TIMING.PROCESSED_FILL_PERSIST_BATCH_SIZE;
|
|
176
|
-
this._processedFillStore = new ProcessedFillStore({
|
|
204
|
+
this._processedFillPersistBatchMs = constants_1.TIMING.PROCESSED_FILL_PERSIST_BATCH_MS;
|
|
205
|
+
this._processedFillPersistBatchSize = constants_1.TIMING.PROCESSED_FILL_PERSIST_BATCH_SIZE;
|
|
206
|
+
this._processedFillStore = new processed_fill_store_1.ProcessedFillStore({
|
|
177
207
|
batchMs: this._processedFillPersistBatchMs,
|
|
178
208
|
batchSize: this._processedFillPersistBatchSize,
|
|
179
209
|
warn: (message) => this._warn(message)
|
|
@@ -227,10 +257,6 @@ class DEXBot {
|
|
|
227
257
|
this._currentCycleId = 0;
|
|
228
258
|
this._autoCancelOrphanCycleMarker = null;
|
|
229
259
|
this._autoCancelOrphanSubCount = 0;
|
|
230
|
-
// Per-session guard: ghost order IDs successfully cancelled
|
|
231
|
-
// (avoids spamming the chain with repeated cancel attempts for the same
|
|
232
|
-
// orphan residual on every fill cycle).
|
|
233
|
-
this._ghostOrderCancelAttempted = null;
|
|
234
260
|
// Dust cancellation uses immediate on-chain cancel — no maps or timers needed.
|
|
235
261
|
this._dustHealthCheckTimer = null;
|
|
236
262
|
// Fill consumer watchdog: consecutive failure tracking.
|
|
@@ -273,7 +299,7 @@ class DEXBot {
|
|
|
273
299
|
}
|
|
274
300
|
// Throw all validation errors at once
|
|
275
301
|
if (errors.length > 0) {
|
|
276
|
-
throw new Error(`Config validation failed:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
|
302
|
+
throw new Error(`Config validation failed:\n${errors.map((e) => ` - ${e}`).join('\n')}`);
|
|
277
303
|
}
|
|
278
304
|
}
|
|
279
305
|
/**
|
|
@@ -324,21 +350,7 @@ class DEXBot {
|
|
|
324
350
|
* @private
|
|
325
351
|
*/
|
|
326
352
|
async _persistAndRecoverIfNeeded() {
|
|
327
|
-
|
|
328
|
-
const validation = await this.manager.persistGrid();
|
|
329
|
-
if (!validation.isValid) {
|
|
330
|
-
this._warn(`Startup validation failed: ${validation.reason}. Triggering immediate recovery...`);
|
|
331
|
-
// Trigger centralized recovery (Hard Reset)
|
|
332
|
-
const recoveryValidation = await this.manager.accountant._performStateRecovery(this.manager);
|
|
333
|
-
if (recoveryValidation.isValid) {
|
|
334
|
-
this._log(`✓ Startup recovery successful. Persistent state restored.`);
|
|
335
|
-
this.manager._recentFillKeysSnapshot = this._getRecentFillKeysSnapshot();
|
|
336
|
-
await this.manager.persistGrid();
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
this._warn(`Startup recovery failed: ${recoveryValidation.reason}. Bot proceeding with caution.`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
353
|
+
return DexbotStateRecovery.persistAndRecoverIfNeeded(this);
|
|
342
354
|
}
|
|
343
355
|
/**
|
|
344
356
|
* Snapshot the recently queued fill keys as a plain object for crash-durable persistence.
|
|
@@ -348,42 +360,14 @@ class DEXBot {
|
|
|
348
360
|
* @returns {Record<string, number>}
|
|
349
361
|
*/
|
|
350
362
|
_getRecentFillKeysSnapshot() {
|
|
351
|
-
|
|
352
|
-
const now = Date.now();
|
|
353
|
-
// 1. Collect live keys from the in-memory map
|
|
354
|
-
for (const [key, timestamp] of this._recentlyQueuedFills) {
|
|
355
|
-
if (now - Number(timestamp) < this._fillDedupeWindowMs) {
|
|
356
|
-
snapshot[key] = Number(timestamp);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
// 2. Merge with the previous snapshot — carry forward any keys that
|
|
360
|
-
// were present in a prior persist cycle, are still within the
|
|
361
|
-
// dedup window, but have been evicted from _recentlyQueuedFills
|
|
362
|
-
// (e.g. by TTL-based pruning between two persistGrid calls).
|
|
363
|
-
// This prevents a crash + restart from losing keys that were
|
|
364
|
-
// processed but not yet persisted.
|
|
365
|
-
if (this.manager?._recentFillKeysSnapshot) {
|
|
366
|
-
for (const [key, timestamp] of Object.entries(this.manager._recentFillKeysSnapshot)) {
|
|
367
|
-
if (!(key in snapshot) && now - Number(timestamp) < this._fillDedupeWindowMs) {
|
|
368
|
-
snapshot[key] = Number(timestamp);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
return snapshot;
|
|
363
|
+
return DexbotStateRecovery.getRecentFillKeysSnapshot(this);
|
|
373
364
|
}
|
|
374
365
|
/**
|
|
375
366
|
* Get current pipeline signal state for congestion checks.
|
|
376
367
|
* @returns {{incomingFillQueueLength: number, shadowLocks: number, batchInFlight: boolean, recoveryInFlight: boolean, broadcasting: boolean}}
|
|
377
368
|
*/
|
|
378
369
|
_getPipelineSignals() {
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
incomingFillQueueLength: this._incomingFillQueue.length,
|
|
382
|
-
shadowLocks: this.manager?.shadowOrderIds?.size || 0,
|
|
383
|
-
batchInFlight: this._batchInFlight,
|
|
384
|
-
recoveryInFlight: this._recoverySyncInFlight,
|
|
385
|
-
broadcasting: this.manager?.isBroadcastingActive?.() || false
|
|
386
|
-
};
|
|
370
|
+
return dexbot_maintenance_runtime_1.default.getPipelineSignals(this);
|
|
387
371
|
}
|
|
388
372
|
/**
|
|
389
373
|
* Mark that grid activity occurred (updates idle timer).
|
|
@@ -391,8 +375,7 @@ class DEXBot {
|
|
|
391
375
|
* @returns {void}
|
|
392
376
|
*/
|
|
393
377
|
_markGridActivity(reason = 'activity') {
|
|
394
|
-
|
|
395
|
-
this.manager?.logger?.log?.(`[MAINT-IDLE] Activity observed: ${reason}`, 'debug');
|
|
378
|
+
return dexbot_maintenance_runtime_1.default.markGridActivity(this, reason);
|
|
396
379
|
}
|
|
397
380
|
/**
|
|
398
381
|
* Trigger a full state recovery sync (fetch chain + sync from open orders + persist).
|
|
@@ -400,25 +383,7 @@ class DEXBot {
|
|
|
400
383
|
* @returns {Promise<void>}
|
|
401
384
|
*/
|
|
402
385
|
async _triggerStateRecoverySync(reason = 'state recovery sync') {
|
|
403
|
-
|
|
404
|
-
return;
|
|
405
|
-
if (this._recoverySyncInFlight) {
|
|
406
|
-
this.manager.logger.log(`[RECOVERY] Skipping duplicate recovery request: ${reason}`, 'warn');
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
this._recoverySyncInFlight = true;
|
|
410
|
-
try {
|
|
411
|
-
this.manager.logger.log(`Triggering state recovery sync (${reason})...`, 'info');
|
|
412
|
-
await this.manager.fetchAccountTotals(this.accountId);
|
|
413
|
-
const openOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
414
|
-
await this.manager.syncFromOpenOrders(openOrders, { skipAccounting: true });
|
|
415
|
-
if (typeof this.manager.persistGrid === 'function') {
|
|
416
|
-
await this.manager.persistGrid();
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
finally {
|
|
420
|
-
this._recoverySyncInFlight = false;
|
|
421
|
-
}
|
|
386
|
+
return DexbotStateRecovery.triggerStateRecoverySync(this, reason);
|
|
422
387
|
}
|
|
423
388
|
/**
|
|
424
389
|
* Abort the current flow if an illegal state signal was raised.
|
|
@@ -426,14 +391,7 @@ class DEXBot {
|
|
|
426
391
|
* @returns {Promise<boolean>} True if flow was aborted
|
|
427
392
|
*/
|
|
428
393
|
async _abortFlowIfIllegalState(flowContext) {
|
|
429
|
-
|
|
430
|
-
if (!illegalSignal) {
|
|
431
|
-
return false;
|
|
432
|
-
}
|
|
433
|
-
this.manager.logger.log(`[HARD-ABORT] ${flowContext} aborted due to illegal state (${illegalSignal.context}): ${illegalSignal.message}`, 'error');
|
|
434
|
-
await this._triggerStateRecoverySync(`hard-abort ${flowContext}`);
|
|
435
|
-
this._maintenanceCooldownCycles = Math.max(this._maintenanceCooldownCycles, 1);
|
|
436
|
-
return true;
|
|
394
|
+
return DexbotStateRecovery.abortFlowIfIllegalState(this, flowContext);
|
|
437
395
|
}
|
|
438
396
|
/**
|
|
439
397
|
* Handle a hard abort from batch processing due to illegal state or accounting failure.
|
|
@@ -443,24 +401,7 @@ class DEXBot {
|
|
|
443
401
|
* @returns {Promise<Object>} Abort result object
|
|
444
402
|
*/
|
|
445
403
|
async _handleBatchHardAbort(err, phase = 'batch processing', opsCount = 0) {
|
|
446
|
-
|
|
447
|
-
const opsInfo = opsCount > 0 ? ` with ${opsCount} ops` : '';
|
|
448
|
-
if (err?.code === 'ILLEGAL_ORDER_STATE') {
|
|
449
|
-
const illegalSignal = this.manager.consumeIllegalStateSignal?.();
|
|
450
|
-
await this._triggerStateRecoverySync(illegalSignal?.message || `illegal order state during ${phase}${opsInfo}`);
|
|
451
|
-
this._maintenanceCooldownCycles = Math.max(this._maintenanceCooldownCycles, 1);
|
|
452
|
-
return { ...baseResult, abortedForIllegalState: true };
|
|
453
|
-
}
|
|
454
|
-
if (err?.code === 'ACCOUNTING_COMMITMENT_FAILED') {
|
|
455
|
-
const accountingSignal = this.manager.consumeAccountingFailureSignal?.();
|
|
456
|
-
const reason = accountingSignal
|
|
457
|
-
? `accounting lock failure (${accountingSignal.side} ${Format.formatAmount8(accountingSignal.amount)}) during ${accountingSignal.context}`
|
|
458
|
-
: `accounting commitment lock failure during ${phase}${opsInfo}`;
|
|
459
|
-
await this._triggerStateRecoverySync(reason);
|
|
460
|
-
this._maintenanceCooldownCycles = Math.max(this._maintenanceCooldownCycles, 1);
|
|
461
|
-
return { ...baseResult, abortedForAccountingFailure: true };
|
|
462
|
-
}
|
|
463
|
-
return null;
|
|
404
|
+
return DexbotStateRecovery.handleBatchHardAbort(this, err, phase, opsCount);
|
|
464
405
|
}
|
|
465
406
|
/**
|
|
466
407
|
* Apply recoverable grid updates (order virtualisation) after a batch failure.
|
|
@@ -469,30 +410,7 @@ class DEXBot {
|
|
|
469
410
|
* @returns {Promise<number>} Number of updates applied
|
|
470
411
|
*/
|
|
471
412
|
async _applyRecoverableGridUpdates(updates, context = 'recoverable-grid-update') {
|
|
472
|
-
|
|
473
|
-
return 0;
|
|
474
|
-
}
|
|
475
|
-
let applied;
|
|
476
|
-
if (typeof this.manager.applyGridUpdateBatch === 'function') {
|
|
477
|
-
await this.manager.applyGridUpdateBatch(updates, context);
|
|
478
|
-
applied = updates.length;
|
|
479
|
-
}
|
|
480
|
-
else {
|
|
481
|
-
applied = 0;
|
|
482
|
-
for (const update of updates) {
|
|
483
|
-
if (typeof this.manager._updateOrder !== 'function')
|
|
484
|
-
break;
|
|
485
|
-
await this.manager._updateOrder(update, context);
|
|
486
|
-
applied++;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
// Persist master grid mutations applied outside COW (stale-order
|
|
490
|
-
// virtualization, size-drift corrections). These run in the COW catch
|
|
491
|
-
// handler where the success-path persistGrid is never reached.
|
|
492
|
-
if (applied > 0 && typeof this.manager.persistGrid === 'function') {
|
|
493
|
-
await this.manager.persistGrid();
|
|
494
|
-
}
|
|
495
|
-
return applied;
|
|
413
|
+
return DexbotStateRecovery.applyRecoverableGridUpdates(this, updates, context);
|
|
496
414
|
}
|
|
497
415
|
/**
|
|
498
416
|
* Recover from explicit stale order errors by virtualizing affected grid slots.
|
|
@@ -501,36 +419,7 @@ class DEXBot {
|
|
|
501
419
|
* @returns {Promise<{executed: boolean, hadRotation: boolean, stale: boolean, recoveredByVirtualization?: boolean}>}
|
|
502
420
|
*/
|
|
503
421
|
async _recoverExplicitStaleOrders(staleOrderIds, reason = 'stale order cleanup') {
|
|
504
|
-
|
|
505
|
-
if (staleIds.length === 0) {
|
|
506
|
-
return { executed: false, hadRotation: false, stale: false };
|
|
507
|
-
}
|
|
508
|
-
this.manager.logger.log(`[COW] Stale order(s) detected: ${staleIds.join(', ')}. Applying targeted cleanup.`, 'warn');
|
|
509
|
-
const updates = [];
|
|
510
|
-
for (const [, gridOrder] of this.manager.orders.entries()) {
|
|
511
|
-
if (!gridOrder?.orderId || !staleOrderIds.has(gridOrder.orderId))
|
|
512
|
-
continue;
|
|
513
|
-
this._staleCleanedOrderIds.set(gridOrder.orderId, Date.now());
|
|
514
|
-
updates.push({ ...virtualizeOrder(gridOrder), size: 0 });
|
|
515
|
-
}
|
|
516
|
-
// Register any stale IDs that had no matching grid slot
|
|
517
|
-
for (const orderId of staleIds) {
|
|
518
|
-
if (!this._staleCleanedOrderIds.has(orderId)) {
|
|
519
|
-
this._staleCleanedOrderIds.set(orderId, Date.now());
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
if (updates.length > 0) {
|
|
523
|
-
await this._applyRecoverableGridUpdates(updates, reason);
|
|
524
|
-
}
|
|
525
|
-
else {
|
|
526
|
-
this.manager.logger.log(`[COW] No local grid slot matched stale order cleanup request (${staleIds.join(', ')}).`, 'debug');
|
|
527
|
-
}
|
|
528
|
-
return {
|
|
529
|
-
executed: false,
|
|
530
|
-
hadRotation: false,
|
|
531
|
-
stale: true,
|
|
532
|
-
recoveredByVirtualization: updates.length > 0
|
|
533
|
-
};
|
|
422
|
+
return DexbotStateRecovery.recoverExplicitStaleOrders(this, staleOrderIds, reason);
|
|
534
423
|
}
|
|
535
424
|
/**
|
|
536
425
|
* Recover from on-chain size drift detected during batch broadcast.
|
|
@@ -538,32 +427,7 @@ class DEXBot {
|
|
|
538
427
|
* @returns {Promise<{executed: boolean, hadRotation: boolean, recoveredBySync: boolean, reason: string}>}
|
|
539
428
|
*/
|
|
540
429
|
async _recoverBatchSizeDrift(err, opContexts = []) {
|
|
541
|
-
|
|
542
|
-
// operation contexts and correct them directly from chain. This
|
|
543
|
-
// avoids a full state recovery sync in the common single-order case.
|
|
544
|
-
const affectedOrderIds = this._extractSizeDriftOrderIds(opContexts);
|
|
545
|
-
if (affectedOrderIds.length > 0) {
|
|
546
|
-
this.manager.logger.log(`[COW] Targeted size-drift repair for ${affectedOrderIds.length} order(s): ${affectedOrderIds.join(', ')}`, 'debug');
|
|
547
|
-
const repaired = await this._targetedOrderRepair(affectedOrderIds);
|
|
548
|
-
if (repaired) {
|
|
549
|
-
return {
|
|
550
|
-
executed: false,
|
|
551
|
-
hadRotation: false,
|
|
552
|
-
recoveredBySync: true,
|
|
553
|
-
reason: 'ORDER_SIZE_DRIFT_TARGETED'
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
this.manager.logger.log('[COW] Targeted repair failed, falling back to full state recovery sync.', 'warn');
|
|
557
|
-
}
|
|
558
|
-
const reason = `recoverable size drift during COW batch: ${err.message}`;
|
|
559
|
-
this.manager.logger.log(`[COW] Recovering from on-chain size drift via recovery sync: ${err.message}`, 'warn');
|
|
560
|
-
await this._triggerStateRecoverySync(reason);
|
|
561
|
-
return {
|
|
562
|
-
executed: false,
|
|
563
|
-
hadRotation: false,
|
|
564
|
-
recoveredBySync: true,
|
|
565
|
-
reason: 'ORDER_SIZE_DRIFT'
|
|
566
|
-
};
|
|
430
|
+
return DexbotStateRecovery.recoverBatchSizeDrift(this, err, opContexts);
|
|
567
431
|
}
|
|
568
432
|
/**
|
|
569
433
|
* Extract chain order IDs from opContexts for operations that could
|
|
@@ -572,18 +436,7 @@ class DEXBot {
|
|
|
572
436
|
* @returns {string[]} Unique chain order IDs
|
|
573
437
|
*/
|
|
574
438
|
_extractSizeDriftOrderIds(opContexts) {
|
|
575
|
-
|
|
576
|
-
return [];
|
|
577
|
-
const ids = new Set();
|
|
578
|
-
for (const ctx of opContexts) {
|
|
579
|
-
if (ctx?.kind === 'size-update' && ctx?.updateInfo?.partialOrder?.orderId) {
|
|
580
|
-
ids.add(ctx.updateInfo.partialOrder.orderId);
|
|
581
|
-
}
|
|
582
|
-
else if (ctx?.kind === 'rotation' && ctx?.rotation?.oldOrder?.orderId) {
|
|
583
|
-
ids.add(ctx.rotation.oldOrder.orderId);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
return Array.from(ids);
|
|
439
|
+
return DexbotStateRecovery.extractSizeDriftOrderIds(opContexts);
|
|
587
440
|
}
|
|
588
441
|
/**
|
|
589
442
|
* Reload the entire grid from the persisted on-disk snapshot and reconcile
|
|
@@ -596,87 +449,7 @@ class DEXBot {
|
|
|
596
449
|
* @returns {Promise<{success: boolean, reason?: string}>}
|
|
597
450
|
*/
|
|
598
451
|
async _recoverFromPersistedGrid() {
|
|
599
|
-
|
|
600
|
-
return { success: false, reason: 'accountOrders or manager unavailable' };
|
|
601
|
-
}
|
|
602
|
-
const accountRef = this.accountId || this.account?.id || this.account;
|
|
603
|
-
if (!accountRef) {
|
|
604
|
-
return { success: false, reason: 'no account reference' };
|
|
605
|
-
}
|
|
606
|
-
this.manager.logger.log('[RECOVERY] Attempting full grid reload from persisted snapshot...', 'warn');
|
|
607
|
-
try {
|
|
608
|
-
// 1. Force reload from disk
|
|
609
|
-
const persistedGrid = this.accountOrders.loadGrid(true);
|
|
610
|
-
if (!persistedGrid || persistedGrid.length === 0) {
|
|
611
|
-
return { success: false, reason: 'no persisted grid on disk' };
|
|
612
|
-
}
|
|
613
|
-
const boundaryIdx = this.accountOrders.loadBoundaryIdx(true);
|
|
614
|
-
// 2. Load into manager (same path as startup at line 1340)
|
|
615
|
-
await Grid.loadGrid(this.manager, persistedGrid, boundaryIdx);
|
|
616
|
-
// Gap 1: Grid snapshot sanity check — shared logic with startup path.
|
|
617
|
-
if (await this._rejectCorruptedGridSnapshot('recovery')) {
|
|
618
|
-
return { success: false, reason: 'corrupted grid snapshot rejected (fund drift)' };
|
|
619
|
-
}
|
|
620
|
-
// 3. Read current chain state
|
|
621
|
-
const chainOpenOrders = await chainOrders.readOpenOrders(accountRef);
|
|
622
|
-
// 4. Reconcile
|
|
623
|
-
if (chainOpenOrders.length > 0 && this.manager?.syncFromOpenOrders) {
|
|
624
|
-
await this.manager.syncFromOpenOrders(chainOpenOrders, {
|
|
625
|
-
skipAccounting: true,
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
// 5. Persist the reconciled state
|
|
629
|
-
if (typeof this.manager.persistGrid === 'function') {
|
|
630
|
-
await this.manager.persistGrid();
|
|
631
|
-
}
|
|
632
|
-
const assets = this.manager?.assets;
|
|
633
|
-
const matchedCount = assets
|
|
634
|
-
? chainOpenOrders.filter(o => parseChainOrder(o, assets) !== null).length
|
|
635
|
-
: chainOpenOrders.length;
|
|
636
|
-
this.manager.logger.log(`[RECOVERY] Grid reloaded from persisted snapshot: ${this.manager.orders.size} orders, ` +
|
|
637
|
-
`${matchedCount} on-chain orders synced`, 'info');
|
|
638
|
-
// Gap 2: Check for unmatched chain orders after reload + sync.
|
|
639
|
-
// If the sync resolved all unmatched entries, _lastUnmatchedChainOrders
|
|
640
|
-
// was cleared by the sync engine. If any remain, the persisted snapshot
|
|
641
|
-
// produced an inconsistent grid — reject so the structural resync
|
|
642
|
-
// falls through to requestGridReset (full rebuild from chain).
|
|
643
|
-
const remainingUnmatched = Array.isArray(this.manager?._lastUnmatchedChainOrders)
|
|
644
|
-
? this.manager._lastUnmatchedChainOrders
|
|
645
|
-
: [];
|
|
646
|
-
if (remainingUnmatched.length > 0) {
|
|
647
|
-
const sample = remainingUnmatched.slice(0, 3)
|
|
648
|
-
.map(o => this._formatUnmatchedChainOrderForLog(o))
|
|
649
|
-
.join(' | ');
|
|
650
|
-
this.manager.logger.log(`[RECOVERY] Persisted grid reloaded but ${remainingUnmatched.length} unmatched chain order(s) ` +
|
|
651
|
-
`remain${sample ? ` (${sample})` : ''}. Rejecting — full grid reset required.`, 'warn');
|
|
652
|
-
return { success: false, reason: `grid inconsistent after reload: ${remainingUnmatched.length} unmatched remain` };
|
|
653
|
-
}
|
|
654
|
-
// If the reloaded grid is still bloated, reject recovery so the
|
|
655
|
-
// caller falls through to a full grid reset (requestGridReset).
|
|
656
|
-
// Without this check, a bloated snapshot gets accepted as "success"
|
|
657
|
-
// and the structural-resync loop loads the same broken state forever.
|
|
658
|
-
//
|
|
659
|
-
// NOTE: loadGrid() already fires requestStructuralGridResync when it
|
|
660
|
-
// detects bloat internally, so the inner async resync may be in flight
|
|
661
|
-
// by the time this outer check runs. That's fine — the structural-resync
|
|
662
|
-
// gate (_structuralGridResyncRunning / _structuralGridResyncTimer) dedup's
|
|
663
|
-
// concurrent requests. This outer check exists so the synchronous return
|
|
664
|
-
// value is honest about the state; the inner resync is a safety net.
|
|
665
|
-
const { isGridBloated } = require('./order/grid');
|
|
666
|
-
const ordersArr = Array.from(this.manager.orders.values());
|
|
667
|
-
const bloatPostRecovery = isGridBloated(this.manager, ordersArr);
|
|
668
|
-
if (bloatPostRecovery.bloated) {
|
|
669
|
-
const d = bloatPostRecovery.details;
|
|
670
|
-
this.manager.logger.log(`[RECOVERY] Persisted grid reloaded but still bloated ` +
|
|
671
|
-
`(${d.gridSize} slots, max ${d.maxAllowed}). Rejecting — full grid reset required.`, 'warn');
|
|
672
|
-
return { success: false, reason: 'grid still bloated after reload' };
|
|
673
|
-
}
|
|
674
|
-
return { success: true };
|
|
675
|
-
}
|
|
676
|
-
catch (err) {
|
|
677
|
-
this.manager.logger.log(`[RECOVERY] Full grid reload from persisted snapshot failed: ${err.message}`, 'error');
|
|
678
|
-
return { success: false, reason: err.message };
|
|
679
|
-
}
|
|
452
|
+
return DexbotStateRecovery.recoverFromPersistedGrid(this);
|
|
680
453
|
}
|
|
681
454
|
/**
|
|
682
455
|
* Reject a corrupted grid snapshot when catastrophic fund drift is detected.
|
|
@@ -687,25 +460,7 @@ class DEXBot {
|
|
|
687
460
|
* @returns {Promise<boolean>} True if the snapshot was rejected (cleared).
|
|
688
461
|
*/
|
|
689
462
|
async _rejectCorruptedGridSnapshot(context) {
|
|
690
|
-
|
|
691
|
-
return false;
|
|
692
|
-
const driftCheck = this.manager.checkFundDriftAfterFills();
|
|
693
|
-
if (driftCheck.isValid)
|
|
694
|
-
return false;
|
|
695
|
-
const tag = context === 'recovery' ? '[RECOVERY][SNAPSHOT-REJECT]' : '[SNAPSHOT-REJECT]';
|
|
696
|
-
this._warn(`${tag} Corrupted grid snapshot detected: ` +
|
|
697
|
-
`drift sell=${driftCheck.driftSell.toFixed(2)} buy=${driftCheck.driftBuy.toFixed(2)}. ` +
|
|
698
|
-
`Deleting corrupted snapshot.`);
|
|
699
|
-
if (this.accountOrders && typeof this.accountOrders.clearGrid === 'function') {
|
|
700
|
-
try {
|
|
701
|
-
await this.accountOrders.clearGrid();
|
|
702
|
-
this._warn(`${tag} Corrupted grid snapshot deleted.`);
|
|
703
|
-
}
|
|
704
|
-
catch (clearErr) {
|
|
705
|
-
this._warn(`${tag} Failed to delete corrupted snapshot: ${clearErr.message}`);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
return true;
|
|
463
|
+
return DexbotStateRecovery.rejectCorruptedGridSnapshot(this, context);
|
|
709
464
|
}
|
|
710
465
|
/**
|
|
711
466
|
* Attempt to repair size-drift for specific order IDs by reading their
|
|
@@ -715,141 +470,16 @@ class DEXBot {
|
|
|
715
470
|
* @returns {Promise<boolean>} True if all affected orders were repaired
|
|
716
471
|
*/
|
|
717
472
|
async _targetedOrderRepair(orderIds) {
|
|
718
|
-
|
|
719
|
-
const objects = await BitShares.db.get_objects(orderIds);
|
|
720
|
-
if (!Array.isArray(objects) || objects.length !== orderIds.length)
|
|
721
|
-
return false;
|
|
722
|
-
const updates = [];
|
|
723
|
-
for (let i = 0; i < orderIds.length; i++) {
|
|
724
|
-
const chainOrder = objects[i];
|
|
725
|
-
const gridOrder = Array.from(this.manager.orders.values())
|
|
726
|
-
.find((o) => o.orderId === orderIds[i]);
|
|
727
|
-
if (!gridOrder)
|
|
728
|
-
continue;
|
|
729
|
-
if (!chainOrder || typeof chainOrder.for_sale === 'undefined') {
|
|
730
|
-
// Order no longer exists on chain -> fully filled or cancelled.
|
|
731
|
-
updates.push({ ...virtualizeOrder(gridOrder), size: 0 });
|
|
732
|
-
}
|
|
733
|
-
else {
|
|
734
|
-
const chainUnits = Number(chainOrder.for_sale);
|
|
735
|
-
if (Number.isFinite(chainUnits)) {
|
|
736
|
-
const { blockchainToFloat } = require('./order/utils/math');
|
|
737
|
-
const prec = gridOrder.type === ORDER_TYPES.SELL
|
|
738
|
-
? this.manager.assets.assetA.precision
|
|
739
|
-
: this.manager.assets.assetB.precision;
|
|
740
|
-
const floatSize = blockchainToFloat(chainUnits, prec);
|
|
741
|
-
if (floatSize !== gridOrder.size) {
|
|
742
|
-
updates.push({
|
|
743
|
-
id: gridOrder.id,
|
|
744
|
-
size: floatSize,
|
|
745
|
-
rawOnChain: chainOrder,
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
if (updates.length > 0) {
|
|
752
|
-
await this._applyRecoverableGridUpdates(updates, 'targeted-size-drift-repair');
|
|
753
|
-
}
|
|
754
|
-
return true;
|
|
755
|
-
}
|
|
756
|
-
catch (err) {
|
|
757
|
-
this.manager.logger.log(`[COW] Targeted order repair failed: ${err.message}`, 'debug');
|
|
758
|
-
return false;
|
|
759
|
-
}
|
|
473
|
+
return DexbotStateRecovery.targetedOrderRepair(this, orderIds);
|
|
760
474
|
}
|
|
761
475
|
/**
|
|
762
476
|
* Initialize bot state from storage and blockchain.
|
|
763
477
|
* Consolidates common initialization logic for start() and startWithPrivateKey().
|
|
764
|
-
* @returns {{persistedGrid: Object, persistedBtsFeesOwed: number, persistedBoundaryIdx: number, persistedBtsBalance: number}}
|
|
478
|
+
* @returns {{persistedGrid: Object, persistedBtsFeesOwed: number, persistedBoundaryIdx: number, persistedBtsBalance: number, persistedRecentFillKeys: Object}}
|
|
765
479
|
* @private
|
|
766
480
|
*/
|
|
767
481
|
async _initializeStartupState() {
|
|
768
|
-
|
|
769
|
-
this.accountOrders = new AccountOrders({ botKey: this.config.botKey });
|
|
770
|
-
this._processedFillStore.configure({
|
|
771
|
-
accountOrders: this.accountOrders
|
|
772
|
-
});
|
|
773
|
-
// Load persisted processed fills to prevent reprocessing after restart
|
|
774
|
-
const loadedPersistedFills = this._processedFillStore.loadPersisted({
|
|
775
|
-
minTimestamp: Date.now() - this._fillRecordRetentionMs
|
|
776
|
-
});
|
|
777
|
-
if (loadedPersistedFills > 0) {
|
|
778
|
-
this._log(`Loaded ${loadedPersistedFills} persisted fill records to prevent reprocessing`);
|
|
779
|
-
}
|
|
780
|
-
// Ensure bot metadata is properly initialized in storage BEFORE any Grid operations
|
|
781
|
-
const raw = storage.readFile(PROFILES_BOTS_FILE);
|
|
782
|
-
const allBotsConfig = parseJsonWithComments(raw).bots || [];
|
|
783
|
-
const myBotConfig = allBotsConfig
|
|
784
|
-
.map((b, originalIdx) => b.active !== false ? normalizeBotEntry(b, originalIdx) : null)
|
|
785
|
-
.find(b => b && b.botKey === this.config.botKey);
|
|
786
|
-
if (myBotConfig) {
|
|
787
|
-
await this.accountOrders.syncMeta(myBotConfig);
|
|
788
|
-
}
|
|
789
|
-
if (!this.manager) {
|
|
790
|
-
const mgrLogFile = this.config?.name ? path.join(PATHS.LOGS_DIR, `${this.config.name}.log`) : undefined;
|
|
791
|
-
this.manager = new OrderManager({ ...this.config, logFile: mgrLogFile });
|
|
792
|
-
this.manager.account = this.account;
|
|
793
|
-
this.manager.accountId = this.accountId;
|
|
794
|
-
this.manager.accountOrders = this.accountOrders;
|
|
795
|
-
}
|
|
796
|
-
this._wireStructuralGridResyncRequest();
|
|
797
|
-
this._wireProcessedFillTracking();
|
|
798
|
-
this.manager.startBootstrap();
|
|
799
|
-
try {
|
|
800
|
-
// Fetch account totals from blockchain at startup to initialize funds
|
|
801
|
-
try {
|
|
802
|
-
if (this.accountId && this.config.assetA && this.config.assetB) {
|
|
803
|
-
await this.manager._initializeAssets();
|
|
804
|
-
await this.manager.fetchAccountTotals(this.accountId);
|
|
805
|
-
this._log('Fetched blockchain account balances at startup');
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
catch (err) {
|
|
809
|
-
this._log(`Startup balance fetch FAILED: ${err.message}. Order sizing may be incorrect until next successful sync.`, 'error');
|
|
810
|
-
}
|
|
811
|
-
// Ensure fee cache is initialized before any fill processing that calls getAssetFees().
|
|
812
|
-
try {
|
|
813
|
-
await initializeFeeCache([this.config || {}], BitShares);
|
|
814
|
-
}
|
|
815
|
-
catch (err) {
|
|
816
|
-
this._log(`Fee cache initialization FAILED: ${err.message}. Fee calculations will use defaults until cache is refreshed.`, 'error');
|
|
817
|
-
}
|
|
818
|
-
const persistedGrid = this.accountOrders.loadGrid();
|
|
819
|
-
// CRITICAL REPAIR: Strip fake orderIds where orderId === id (e.g. "slot-0")
|
|
820
|
-
let repairedGrid = persistedGrid;
|
|
821
|
-
if (persistedGrid && persistedGrid.length > 0) {
|
|
822
|
-
let repairCount = 0;
|
|
823
|
-
repairedGrid = persistedGrid.map(order => {
|
|
824
|
-
if (order && order.orderId && order.orderId === order.id) {
|
|
825
|
-
repairCount++;
|
|
826
|
-
const repairedOrder = { ...order, orderId: '' };
|
|
827
|
-
if (repairedOrder.state === ORDER_STATES.ACTIVE || repairedOrder.state === ORDER_STATES.PARTIAL) {
|
|
828
|
-
repairedOrder.state = ORDER_STATES.VIRTUAL;
|
|
829
|
-
}
|
|
830
|
-
return repairedOrder;
|
|
831
|
-
}
|
|
832
|
-
return order;
|
|
833
|
-
});
|
|
834
|
-
if (repairCount > 0) {
|
|
835
|
-
this._log(`[REPAIR] Stripped ${repairCount} fake orderId(s) from persisted grid to restore rebalancing logic.`);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
const persistedBtsFeesOwed = this.accountOrders.loadBtsFeesOwed();
|
|
839
|
-
const persistedBoundaryIdx = this.accountOrders.loadBoundaryIdx();
|
|
840
|
-
const persistedBtsBalance = this.accountOrders.loadBtsBalance();
|
|
841
|
-
const persistedRecentFillKeys = this.accountOrders.loadRecentFillKeys();
|
|
842
|
-
return {
|
|
843
|
-
persistedGrid: repairedGrid,
|
|
844
|
-
persistedBtsFeesOwed,
|
|
845
|
-
persistedBoundaryIdx,
|
|
846
|
-
persistedBtsBalance,
|
|
847
|
-
persistedRecentFillKeys,
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
finally {
|
|
851
|
-
this.manager.finishBootstrap();
|
|
852
|
-
}
|
|
482
|
+
return DexbotStartupRuntime.initializeStartupState(this);
|
|
853
483
|
}
|
|
854
484
|
/**
|
|
855
485
|
* Wire processed fill tracking into the manager.
|
|
@@ -928,7 +558,7 @@ class DEXBot {
|
|
|
928
558
|
* @param {boolean} [options.allowOrphanFallbackKey=false]
|
|
929
559
|
* @returns {Promise<import('./types').ReplaySafeFillResult>}
|
|
930
560
|
*/
|
|
931
|
-
async _applyReplaySafeFillAccounting(fill, fillOp, { missingKeyMessage, fallbackKeyMessage, replayMessage, errorMessage, logger = this.manager?.logger, missingKeyLevel = 'warn', fallbackKeyLevel = 'warn', replayLevel = 'debug', persistenceMode = PROCESSED_FILL_PERSISTENCE_MODES.IMMEDIATE, allowOrphanFallbackKey = false } = {}) {
|
|
561
|
+
async _applyReplaySafeFillAccounting(fill, fillOp, { missingKeyMessage, fallbackKeyMessage, replayMessage, errorMessage, logger = this.manager?.logger, missingKeyLevel = 'warn', fallbackKeyLevel = 'warn', replayLevel = 'debug', persistenceMode = processed_fill_store_1.PROCESSED_FILL_PERSISTENCE_MODES.IMMEDIATE, allowOrphanFallbackKey = false } = {}) {
|
|
932
562
|
return DexbotFillRuntime.applyReplaySafeFillAccounting(this, fill, fillOp, {
|
|
933
563
|
missingKeyMessage,
|
|
934
564
|
fallbackKeyMessage,
|
|
@@ -953,7 +583,7 @@ class DEXBot {
|
|
|
953
583
|
* @param {string} [options.persistenceMode='batched']
|
|
954
584
|
* @returns {Promise<import('./types').ReplaySafeFillResult>}
|
|
955
585
|
*/
|
|
956
|
-
async _applyReplaySafeTrackedFillAccounting(fill, fillOp, { context, logger = this.manager?.logger, replayMessage, persistenceMode = PROCESSED_FILL_PERSISTENCE_MODES.BATCHED } = {}) {
|
|
586
|
+
async _applyReplaySafeTrackedFillAccounting(fill, fillOp, { context, logger = this.manager?.logger, replayMessage, persistenceMode = processed_fill_store_1.PROCESSED_FILL_PERSISTENCE_MODES.BATCHED } = {}) {
|
|
957
587
|
return DexbotFillRuntime.applyReplaySafeTrackedFillAccounting(this, fill, fillOp, {
|
|
958
588
|
context,
|
|
959
589
|
logger,
|
|
@@ -972,7 +602,7 @@ class DEXBot {
|
|
|
972
602
|
* @param {string} [options.persistenceMode='batched']
|
|
973
603
|
* @returns {Promise<import('./types').ReplaySafeFillResult>}
|
|
974
604
|
*/
|
|
975
|
-
async _applyReplaySafeOrphanFillAccounting(fill, fillOp, { context, logger = this.manager?.logger, replayMessage, persistenceMode = PROCESSED_FILL_PERSISTENCE_MODES.BATCHED } = {}) {
|
|
605
|
+
async _applyReplaySafeOrphanFillAccounting(fill, fillOp, { context, logger = this.manager?.logger, replayMessage, persistenceMode = processed_fill_store_1.PROCESSED_FILL_PERSISTENCE_MODES.BATCHED } = {}) {
|
|
976
606
|
return DexbotFillRuntime.applyReplaySafeOrphanFillAccounting(this, fill, fillOp, {
|
|
977
607
|
context,
|
|
978
608
|
logger,
|
|
@@ -986,7 +616,7 @@ class DEXBot {
|
|
|
986
616
|
* @returns {import('./types').DynamicWeightRefreshResult|null}
|
|
987
617
|
*/
|
|
988
618
|
_refreshDynamicWeightDistribution(context = 'runtime') {
|
|
989
|
-
return
|
|
619
|
+
return dexbot_maintenance_runtime_1.default.refreshDynamicWeightDistribution(this, context);
|
|
990
620
|
}
|
|
991
621
|
/**
|
|
992
622
|
* Finalize the bot startup after account and initial grid sync are complete.
|
|
@@ -995,458 +625,7 @@ class DEXBot {
|
|
|
995
625
|
* @private
|
|
996
626
|
*/
|
|
997
627
|
async _finishStartupSequence(startupState) {
|
|
998
|
-
|
|
999
|
-
try {
|
|
1000
|
-
// CRITICAL: Activate fill listener EARLY - before ANY operations that place orders
|
|
1001
|
-
// This ensures fills during trigger reset and grid initialization are captured
|
|
1002
|
-
if (typeof this._fillsUnsubscribe === 'function') {
|
|
1003
|
-
await this._fillsUnsubscribe().catch(() => { });
|
|
1004
|
-
}
|
|
1005
|
-
this._fillsUnsubscribe = await chainOrders.listenForFills(this.account || undefined, this._createFillCallback(chainOrders));
|
|
1006
|
-
if (typeof this._fillsUnsubscribe !== 'function') {
|
|
1007
|
-
this._warn('Fill listener did not provide an unsubscribe handler. Shutdown cleanup may be incomplete.');
|
|
1008
|
-
this._fillsUnsubscribe = null;
|
|
1009
|
-
}
|
|
1010
|
-
this._log('Fill listener activated (ready to process fills during startup)');
|
|
1011
|
-
// Register reconnection callback for safety-net sync after websocket reconnect
|
|
1012
|
-
if (!this._reconnectUnregister) {
|
|
1013
|
-
this._reconnectUnregister = registerReconnectHook(() => {
|
|
1014
|
-
this._log('Blockchain connection re-established; scheduling safety-net sync');
|
|
1015
|
-
const runSafetyNetSync = async () => {
|
|
1016
|
-
if (this.manager && this.accountId && !this._shuttingDown && !this.config.dryRun) {
|
|
1017
|
-
// Cap the entire safety-net sync at TIMING.SAFETY_NET_SYNC_TIMEOUT_MS so it can
|
|
1018
|
-
// never hold _fillProcessingLock longer than the
|
|
1019
|
-
// 20s shutdown lock timeout. readOpenOrders +
|
|
1020
|
-
// synchronizeWithChain + batch + persist can be
|
|
1021
|
-
// ~45s worst case without a cap, which would
|
|
1022
|
-
// stall shutdown until the 20s timeout fires.
|
|
1023
|
-
const safetyNetTimeoutMs = this.config.timing?.SAFETY_NET_SYNC_TIMEOUT_MS;
|
|
1024
|
-
let safetyNetTimer;
|
|
1025
|
-
const workPromise = this.manager._fillProcessingLock.acquire(async () => {
|
|
1026
|
-
if (this._shuttingDown)
|
|
1027
|
-
return;
|
|
1028
|
-
const chainOpenOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
1029
|
-
if (this._shuttingDown)
|
|
1030
|
-
return;
|
|
1031
|
-
const syncResult = await this.manager.synchronizeWithChain(chainOpenOrders, 'readOpenOrders');
|
|
1032
|
-
if (this._shuttingDown)
|
|
1033
|
-
return;
|
|
1034
|
-
if (syncResult?.filledOrders?.length > 0) {
|
|
1035
|
-
this._refreshDynamicWeightDistribution('post-reconnect sync fill');
|
|
1036
|
-
this._log(`Post-reconnect sync: ${syncResult.filledOrders.length} grid order(s) found filled.`, 'info');
|
|
1037
|
-
await this._processFillsWithBatching(syncResult.filledOrders, new Set(), 'post-reconnect sync fill');
|
|
1038
|
-
if (this._shuttingDown)
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
this.manager._recentFillKeysSnapshot = this._getRecentFillKeysSnapshot();
|
|
1042
|
-
await this.manager.persistGrid();
|
|
1043
|
-
// Cancel any dust created by reconnect fills immediately.
|
|
1044
|
-
if (!this._shuttingDown) {
|
|
1045
|
-
try {
|
|
1046
|
-
const reconnectHealth = await this.manager.checkGridHealth(this.updateOrdersOnChainPlan.bind(this));
|
|
1047
|
-
await this._cancelDustOrders({
|
|
1048
|
-
buy: reconnectHealth.buyDustOrders,
|
|
1049
|
-
sell: reconnectHealth.sellDustOrders,
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
catch (_dustErr) {
|
|
1053
|
-
this._warn(`[RECONNECT] Dust cancel failed: ${_dustErr.message}`);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
});
|
|
1057
|
-
try {
|
|
1058
|
-
await Promise.race([
|
|
1059
|
-
workPromise,
|
|
1060
|
-
new Promise((_, reject) => {
|
|
1061
|
-
safetyNetTimer = setTimeout(() => reject(new Error(`Safety-net sync exceeded ${safetyNetTimeoutMs}ms cap`)), safetyNetTimeoutMs);
|
|
1062
|
-
})
|
|
1063
|
-
]);
|
|
1064
|
-
}
|
|
1065
|
-
catch (capErr) {
|
|
1066
|
-
const fallback = await Promise.race([
|
|
1067
|
-
workPromise.then(() => ({ ok: true })),
|
|
1068
|
-
new Promise(resolve => setTimeout(() => resolve({ ok: false }), 0))
|
|
1069
|
-
]);
|
|
1070
|
-
if (fallback.ok) {
|
|
1071
|
-
this._log(`Safety-net sync completed despite timeout — ignoring spurious error.`, 'info');
|
|
1072
|
-
}
|
|
1073
|
-
else {
|
|
1074
|
-
this._warn(`Post-reconnect safety-net sync aborted: ${capErr?.message || capErr}`);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
finally {
|
|
1078
|
-
if (safetyNetTimer)
|
|
1079
|
-
clearTimeout(safetyNetTimer);
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
};
|
|
1083
|
-
// The setImmediate callback returns a Promise; we MUST attach
|
|
1084
|
-
// a .catch so a rejection here does not propagate to
|
|
1085
|
-
// process.on('unhandledRejection') and tear down the bot.
|
|
1086
|
-
setImmediate(() => {
|
|
1087
|
-
runSafetyNetSync().catch(err => {
|
|
1088
|
-
try {
|
|
1089
|
-
this._warn('Post-reconnect safety-net sync failed: ' + (err?.message || err));
|
|
1090
|
-
}
|
|
1091
|
-
catch (_warnErr) {
|
|
1092
|
-
// _warn itself inaccessible — last line of defense.
|
|
1093
|
-
}
|
|
1094
|
-
});
|
|
1095
|
-
});
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
// CRITICAL: Handle any pending trigger file reset FIRST before any other startup operations
|
|
1099
|
-
const hadTriggerReset = await this._handlePendingTriggerReset();
|
|
1100
|
-
// CRITICAL: After trigger reset, skip normal startup - grid is already fully initialized
|
|
1101
|
-
// The trigger reset already did: grid init, order placement, sync, and persistence
|
|
1102
|
-
if (hadTriggerReset) {
|
|
1103
|
-
this._log('Trigger reset completed. Skipping normal startup grid initialization.');
|
|
1104
|
-
// Post-bootstrap validation and fill processing
|
|
1105
|
-
await this.manager._fillProcessingLock.acquire(async () => {
|
|
1106
|
-
// STEP 1: Check for fills that occurred during trigger reset
|
|
1107
|
-
// These are orders that got filled while Grid.recalculateGrid() was running.
|
|
1108
|
-
// The filled slots need new orders placed on them.
|
|
1109
|
-
if (this._incomingFillQueue.length > 0) {
|
|
1110
|
-
this._log(`[POST-RESET] ${this._incomingFillQueue.length} fill(s) detected during trigger reset. Processing...`);
|
|
1111
|
-
// Process fills - this will place new orders on the filled slots
|
|
1112
|
-
// Use normal fill processing since bootstrap is complete
|
|
1113
|
-
const fills = this._incomingFillQueue.splice(0);
|
|
1114
|
-
const processedFillKeys = new Set();
|
|
1115
|
-
let requiresOpenOrdersSync = false;
|
|
1116
|
-
for (const fill of fills) {
|
|
1117
|
-
if (!fill || fill.op?.[0] !== 4)
|
|
1118
|
-
continue;
|
|
1119
|
-
const fillOp = fill.op[1];
|
|
1120
|
-
const gridOrder = this.manager.orders.get(fillOp.order_id) ||
|
|
1121
|
-
Array.from(this.manager.orders.values()).find((o) => o.orderId === fillOp.order_id);
|
|
1122
|
-
if (!gridOrder) {
|
|
1123
|
-
// CRITICAL FIX: Even if order not in grid, we must still credit the fill proceeds
|
|
1124
|
-
// This can happen when fills arrive after an order was marked VIRTUAL during sequential processing
|
|
1125
|
-
let orphanFillKey = buildFillKey(fill);
|
|
1126
|
-
if (!orphanFillKey) {
|
|
1127
|
-
orphanFillKey = this._buildOrphanFillFallbackKey(fill);
|
|
1128
|
-
}
|
|
1129
|
-
if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[POST-RESET]', fillOp.order_id)) {
|
|
1130
|
-
continue;
|
|
1131
|
-
}
|
|
1132
|
-
this._log(`[POST-RESET] Processing funds for unknown order ${fillOp.order_id} (not in grid but crediting proceeds)`, 'warn');
|
|
1133
|
-
const accountingResult = await this._applyReplaySafeOrphanFillAccounting(fill, fillOp, {
|
|
1134
|
-
context: 'POST-RESET',
|
|
1135
|
-
logger: { log: this._log.bind(this) },
|
|
1136
|
-
replayMessage: (op) => `[POST-RESET] Replay detected for orphan fill ${op.order_id}; skipping duplicate credit`
|
|
1137
|
-
});
|
|
1138
|
-
if (accountingResult.status === 'missing_key') {
|
|
1139
|
-
requiresOpenOrdersSync = true;
|
|
1140
|
-
}
|
|
1141
|
-
continue;
|
|
1142
|
-
}
|
|
1143
|
-
this._log(`[POST-RESET] Processing fill for ${gridOrder.type} order ${gridOrder.id} at price ${gridOrder.price}`);
|
|
1144
|
-
const trackedFillKey = buildFillKey(fill);
|
|
1145
|
-
if (trackedFillKey && !this._isNewFillKey(trackedFillKey, processedFillKeys, '[POST-RESET]', fillOp.order_id)) {
|
|
1146
|
-
continue;
|
|
1147
|
-
}
|
|
1148
|
-
this.manager.lockOrders([gridOrder.id]);
|
|
1149
|
-
try {
|
|
1150
|
-
const accountingResult = await this._applyReplaySafeTrackedFillAccounting(fill, fillOp, {
|
|
1151
|
-
context: 'POST-RESET',
|
|
1152
|
-
logger: { log: this._log.bind(this) },
|
|
1153
|
-
replayMessage: (op) => `[POST-RESET] Replay detected for ${op.order_id}; skipping duplicate rebalance`
|
|
1154
|
-
});
|
|
1155
|
-
if (accountingResult.status === 'missing_key') {
|
|
1156
|
-
requiresOpenOrdersSync = true;
|
|
1157
|
-
continue;
|
|
1158
|
-
}
|
|
1159
|
-
if (accountingResult.status !== 'applied') {
|
|
1160
|
-
continue;
|
|
1161
|
-
}
|
|
1162
|
-
// Process this fill through the full rebalance pipeline
|
|
1163
|
-
// This will shift the boundary and place a new order on the filled slot
|
|
1164
|
-
const result = await this._processFillsWithBatching([gridOrder], new Set(), `[POST-RESET] fill ${gridOrder.id}`);
|
|
1165
|
-
if (result.aborted) {
|
|
1166
|
-
this._warn('[POST-RESET] Aborted batch due to illegal state; skipping grid persistence this cycle');
|
|
1167
|
-
continue;
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
finally {
|
|
1171
|
-
this.manager.unlockOrders([gridOrder.id]);
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
if (requiresOpenOrdersSync) {
|
|
1175
|
-
this._log('[POST-RESET] Falling back to open-orders sync for fill(s) missing replay-safe history identifiers', 'warn');
|
|
1176
|
-
const postResetChainOpenOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
1177
|
-
const syncResult = await this.manager.syncFromOpenOrders(postResetChainOpenOrders);
|
|
1178
|
-
if (syncResult.filledOrders?.length > 0) {
|
|
1179
|
-
await this._processFillsWithBatching(syncResult.filledOrders, new Set(), '[POST-RESET] open-orders fallback');
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
await this._flushProcessedFillPersistence('post-reset-batch');
|
|
1183
|
-
this.manager._recentFillKeysSnapshot = this._getRecentFillKeysSnapshot();
|
|
1184
|
-
await this.manager.persistGrid();
|
|
1185
|
-
}
|
|
1186
|
-
// STEP 2: Refresh chain truth before spread correction. Trigger
|
|
1187
|
-
// reset can create/cancel orders and fills can arrive while the
|
|
1188
|
-
// reset is running; spread decisions must not use stale local grid.
|
|
1189
|
-
const { aborted: postResetAborted, hasUnmatched: postResetUnmatched } = await this._syncOpenOrdersAndProcessFills('[POST-RESET] pre-spread');
|
|
1190
|
-
if (postResetUnmatched) {
|
|
1191
|
-
this._warn(`[POST-RESET] Skipping spread correction: ${postResetUnmatched} unmatched chain order(s) require maintenance reconciliation`);
|
|
1192
|
-
}
|
|
1193
|
-
// STEP 3: Spread check AFTER fills are processed and chain truth refreshed
|
|
1194
|
-
await this.manager.recalculateFunds();
|
|
1195
|
-
if (!postResetAborted && !postResetUnmatched) {
|
|
1196
|
-
const spreadResult = await this.manager.checkSpreadCondition(BitShares, this.updateOrdersOnChainPlan.bind(this));
|
|
1197
|
-
if (spreadResult && spreadResult.ordersPlaced > 0) {
|
|
1198
|
-
this._log(`✓ Spread correction after trigger reset: ${spreadResult.ordersPlaced} order(s) placed`);
|
|
1199
|
-
await this._persistAndRecoverIfNeeded();
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
// Cancel any dust created by post-reset fills immediately.
|
|
1203
|
-
if (!this._shuttingDown) {
|
|
1204
|
-
try {
|
|
1205
|
-
const postResetHealth = await this.manager.checkGridHealth(this.updateOrdersOnChainPlan.bind(this));
|
|
1206
|
-
await this._cancelDustOrders({
|
|
1207
|
-
buy: postResetHealth.buyDustOrders,
|
|
1208
|
-
sell: postResetHealth.sellDustOrders,
|
|
1209
|
-
});
|
|
1210
|
-
}
|
|
1211
|
-
catch (_dustErr) {
|
|
1212
|
-
this._warn(`[POST-RESET] Dust cancel failed: ${_dustErr.message}`);
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
this._log('Bootstrap phase complete - fill processing resumed', 'info');
|
|
1216
|
-
});
|
|
1217
|
-
await this._setupTriggerFileDetection();
|
|
1218
|
-
await this._setupCreditRuntime();
|
|
1219
|
-
await this._refreshAndSyncCreditRuntime();
|
|
1220
|
-
this._setupBlockchainFetchInterval();
|
|
1221
|
-
this._setupCreditWatchdogInterval();
|
|
1222
|
-
this._setupCredentialDaemonWatchdogInterval();
|
|
1223
|
-
this._setupDustHealthCheckInterval();
|
|
1224
|
-
await this._runDustHealthCheck();
|
|
1225
|
-
this._log('[DUST] Startup health check complete');
|
|
1226
|
-
if (this._isOpenOrdersSyncLoopEnabled()) {
|
|
1227
|
-
this._startOpenOrdersSyncLoop();
|
|
1228
|
-
}
|
|
1229
|
-
else {
|
|
1230
|
-
this._log('Open-orders sync loop disabled by configuration (TIMING.OPEN_ORDERS_SYNC_LOOP_ENABLED=false)');
|
|
1231
|
-
}
|
|
1232
|
-
this._log(`DEXBot started. OrderManager running (dryRun=${!!this.config.dryRun})`);
|
|
1233
|
-
return; // Skip normal startup path
|
|
1234
|
-
}
|
|
1235
|
-
// Restore persisted BTS fee
|
|
1236
|
-
// SAFE: Done at startup before orders are created, and within fill lock when needed
|
|
1237
|
-
this.manager.resetFunds();
|
|
1238
|
-
// CRITICAL FIX: Restore BTS fees owed from persistence
|
|
1239
|
-
if (persistedBtsFeesOwed && persistedBtsFeesOwed > 0) {
|
|
1240
|
-
this.manager.funds.btsFeesOwed = Number(persistedBtsFeesOwed);
|
|
1241
|
-
}
|
|
1242
|
-
// Restore BTS balance for non-BTS pairs
|
|
1243
|
-
if (this.config.assetA !== 'BTS' && this.config.assetB !== 'BTS') {
|
|
1244
|
-
if (persistedBtsBalance && typeof persistedBtsBalance === 'object') {
|
|
1245
|
-
this.manager.btsBalance = {
|
|
1246
|
-
free: persistedBtsBalance.free || 0,
|
|
1247
|
-
total: persistedBtsBalance.total || 0,
|
|
1248
|
-
locked: persistedBtsBalance.locked || 0
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
// Restore recently queued fill keys for crash-durable dedup window.
|
|
1253
|
-
// All persisted keys are restored; the in-memory TTL (_fillDedupeWindowMs)
|
|
1254
|
-
// will naturally evict any that are already expired on the next fill cycle.
|
|
1255
|
-
if (persistedRecentFillKeys && typeof persistedRecentFillKeys === 'object') {
|
|
1256
|
-
for (const [fillKey, timestamp] of Object.entries(persistedRecentFillKeys)) {
|
|
1257
|
-
this._recentlyQueuedFills.set(fillKey, Number(timestamp));
|
|
1258
|
-
}
|
|
1259
|
-
this._log(`Restored ${Object.keys(persistedRecentFillKeys).length} recently queued fill key(s) from persisted snapshot`, 'debug');
|
|
1260
|
-
}
|
|
1261
|
-
if (!this.config.dryRun && !this.accountId) {
|
|
1262
|
-
throw new Error('Cannot start bot without a resolved account ID');
|
|
1263
|
-
}
|
|
1264
|
-
// Use this.accountId which was set during initialize()
|
|
1265
|
-
const chainOpenOrders = this.config.dryRun ? [] : await chainOrders.readOpenOrders(this.accountId);
|
|
1266
|
-
let shouldRegenerate = false;
|
|
1267
|
-
if (!persistedGrid || persistedGrid.length === 0) {
|
|
1268
|
-
shouldRegenerate = true;
|
|
1269
|
-
this._log('No persisted grid found. Generating new grid.');
|
|
1270
|
-
}
|
|
1271
|
-
else {
|
|
1272
|
-
await this.manager._initializeAssets();
|
|
1273
|
-
const decision = await decideStartupGridAction({
|
|
1274
|
-
persistedGrid,
|
|
1275
|
-
chainOpenOrders,
|
|
1276
|
-
manager: this.manager,
|
|
1277
|
-
logger: { log: (msg) => this._log(msg) },
|
|
1278
|
-
storeGrid: async (orders) => {
|
|
1279
|
-
// Pass the snapshot directly to persistGrid so the live
|
|
1280
|
-
// `manager.orders` map is not briefly swapped (which would
|
|
1281
|
-
// leave the _ordersByState/_ordersByType indexes inconsistent
|
|
1282
|
-
// with the map for the duration of the persist call).
|
|
1283
|
-
await this.manager.persistGrid(orders);
|
|
1284
|
-
},
|
|
1285
|
-
attemptResumeFn: attemptResumePersistedGridByPriceMatch,
|
|
1286
|
-
});
|
|
1287
|
-
shouldRegenerate = decision.shouldRegenerate;
|
|
1288
|
-
if (shouldRegenerate && chainOpenOrders.length === 0) {
|
|
1289
|
-
this._log('Persisted grid found, but no matching active orders on-chain. Generating new grid.');
|
|
1290
|
-
}
|
|
1291
|
-
// Also log when regeneration is needed but other pairs' orders
|
|
1292
|
-
// suppress the "no matching orders" message above.
|
|
1293
|
-
if (shouldRegenerate && chainOpenOrders.length > 0 && this.manager?.assets) {
|
|
1294
|
-
const orderCount = chainOpenOrders.filter(o => parseChainOrder(o, this.manager.assets) !== null).length;
|
|
1295
|
-
if (orderCount === 0) {
|
|
1296
|
-
this._log(`Persisted grid found with no matching orders (${chainOpenOrders.length} other-pair order(s) on account). Generating new grid.`);
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
// Restore BTS fees owed ONLY if we're NOT regenerating the grid
|
|
1301
|
-
if (!shouldRegenerate) {
|
|
1302
|
-
// CRITICAL: Restore BTS fees owed from blockchain operations
|
|
1303
|
-
if (persistedBtsFeesOwed > 0) {
|
|
1304
|
-
this.manager.funds.btsFeesOwed = persistedBtsFeesOwed;
|
|
1305
|
-
this._log(`✓ Restored BTS fees owed: ${Format.formatAmount8(persistedBtsFeesOwed)} BTS`);
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
else {
|
|
1309
|
-
this._log(`ℹ Grid regenerating - resetting BTS fees to clean state`);
|
|
1310
|
-
this.manager.funds.btsFeesOwed = 0;
|
|
1311
|
-
}
|
|
1312
|
-
// CRITICAL: Use fill lock during ENTIRE startup synchronization to prevent races.
|
|
1313
|
-
// This includes grid init, finishBootstrap, and maintenance - all in one atomic block.
|
|
1314
|
-
// Lock order: _fillProcessingLock → _divergenceLock (canonical order, same as _consumeFillQueue)
|
|
1315
|
-
await this.manager._fillProcessingLock.acquire(async () => {
|
|
1316
|
-
try {
|
|
1317
|
-
this._refreshDynamicWeightDistribution('startup');
|
|
1318
|
-
if (shouldRegenerate) {
|
|
1319
|
-
await this.manager._initializeAssets();
|
|
1320
|
-
if (Array.isArray(chainOpenOrders) && chainOpenOrders.length > 0) {
|
|
1321
|
-
this._log('Generating new grid and syncing with existing on-chain orders...');
|
|
1322
|
-
await Grid.initializeGrid(this.manager);
|
|
1323
|
-
await this.manager.syncFromOpenOrders(chainOpenOrders, { skipAccounting: true });
|
|
1324
|
-
const rebalanceResult = await reconcileGridOrders({
|
|
1325
|
-
manager: this.manager,
|
|
1326
|
-
config: this.config,
|
|
1327
|
-
account: this.account,
|
|
1328
|
-
privateKey: this.privateKey,
|
|
1329
|
-
chainOrders,
|
|
1330
|
-
chainOpenOrders,
|
|
1331
|
-
});
|
|
1332
|
-
await this._executeBatchIfNeeded(rebalanceResult, 'startup reconcile (regenerated grid)');
|
|
1333
|
-
}
|
|
1334
|
-
else {
|
|
1335
|
-
this._log('Generating new grid and placing initial orders on-chain...');
|
|
1336
|
-
await this.placeInitialOrders();
|
|
1337
|
-
}
|
|
1338
|
-
await this._persistAndRecoverIfNeeded();
|
|
1339
|
-
}
|
|
1340
|
-
else {
|
|
1341
|
-
this._log('Found active session. Loading and syncing existing grid.');
|
|
1342
|
-
await Grid.loadGrid(this.manager, persistedGrid, persistedBoundaryIdx);
|
|
1343
|
-
let startupChainOpenOrders = chainOpenOrders;
|
|
1344
|
-
const syncResult = await this.manager.syncFromOpenOrders(startupChainOpenOrders, { skipAccounting: true });
|
|
1345
|
-
// Process price corrections queued during startup sync.
|
|
1346
|
-
// These are not picked up by _consumeFillQueue until a fill
|
|
1347
|
-
// arrives, which may never come for an idle market.
|
|
1348
|
-
if (syncResult.ordersNeedingCorrection?.length > 0) {
|
|
1349
|
-
await correctAllPriceMismatches(this.manager, this.account, this.privateKey, chainOrders);
|
|
1350
|
-
}
|
|
1351
|
-
if (syncResult.filledOrders && syncResult.filledOrders.length > 0) {
|
|
1352
|
-
this._log(`Startup sync: ${syncResult.filledOrders.length} grid order(s) found filled. Processing proceeds.`, 'info');
|
|
1353
|
-
const batchResult = await this._processFillsWithBatching(syncResult.filledOrders, new Set(), 'startup sync fill rebalance', { skipAccountTotalsUpdate: true });
|
|
1354
|
-
if (!batchResult?.aborted) {
|
|
1355
|
-
// Refresh open orders so startup reconcile works with post-batch chain reality
|
|
1356
|
-
// and avoids reconciling against a stale pre-batch snapshot.
|
|
1357
|
-
startupChainOpenOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
1358
|
-
await this.manager.synchronizeWithChain(startupChainOpenOrders, 'readOpenOrders');
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
const rebalanceResult = await reconcileGridOrders({
|
|
1362
|
-
manager: this.manager,
|
|
1363
|
-
config: this.config,
|
|
1364
|
-
account: this.account,
|
|
1365
|
-
privateKey: this.privateKey,
|
|
1366
|
-
chainOrders,
|
|
1367
|
-
chainOpenOrders: startupChainOpenOrders,
|
|
1368
|
-
});
|
|
1369
|
-
await this._executeBatchIfNeeded(rebalanceResult, 'startup reconcile (loaded grid)');
|
|
1370
|
-
// Dust state is no longer persisted — cancelled immediately on detection.
|
|
1371
|
-
await this._persistAndRecoverIfNeeded();
|
|
1372
|
-
// Gap 1: Grid snapshot sanity check — shared logic with recovery path.
|
|
1373
|
-
await this._rejectCorruptedGridSnapshot('startup');
|
|
1374
|
-
}
|
|
1375
|
-
// Drain any fills that arrived during startup while still in bootstrap
|
|
1376
|
-
// mode. Safe to call directly since we already hold _fillProcessingLock
|
|
1377
|
-
// and _processFillsWithBootstrapMode does NOT re-acquire it.
|
|
1378
|
-
if (this._incomingFillQueue.length > 0) {
|
|
1379
|
-
this._log(`[STARTUP] Processing ${this._incomingFillQueue.length} queued fill(s) before bootstrap ends`);
|
|
1380
|
-
await this._processFillsWithBootstrapMode(chainOrders);
|
|
1381
|
-
}
|
|
1382
|
-
this.manager.finishBootstrap();
|
|
1383
|
-
// Refresh account totals after bootstrap to eliminate the timing
|
|
1384
|
-
// gap between the initial balance fetch and grid operations (sync,
|
|
1385
|
-
// reconcile, fills). Without this, the first maintenance cycle sees
|
|
1386
|
-
// a fund drift (expected at bootstrap) and triggers an unnecessary
|
|
1387
|
-
// invariant violation + full recovery cycle.
|
|
1388
|
-
// Bound by a timeout to avoid blocking _fillProcessingLock on a
|
|
1389
|
-
// flaky node. If the fetch times out, continue with cached values;
|
|
1390
|
-
// the next periodic maintenance cycle will retry.
|
|
1391
|
-
const FETCH_TIMEOUT_MS = 30000;
|
|
1392
|
-
let _fetchTimeoutHandle;
|
|
1393
|
-
try {
|
|
1394
|
-
await Promise.race([
|
|
1395
|
-
this.manager.fetchAccountTotals(),
|
|
1396
|
-
new Promise((_, reject) => {
|
|
1397
|
-
_fetchTimeoutHandle = setTimeout(() => reject(new Error('timeout')), FETCH_TIMEOUT_MS);
|
|
1398
|
-
})
|
|
1399
|
-
]);
|
|
1400
|
-
}
|
|
1401
|
-
catch (fetchErr) {
|
|
1402
|
-
this._log(`[STARTUP] [${this.config?.botKey || 'unknown'}] fetchAccountTotals ${fetchErr.message === 'timeout' ? 'timed out' : 'failed'} (${fetchErr.message}). Continuing with cached account totals.`, 'warn');
|
|
1403
|
-
}
|
|
1404
|
-
finally {
|
|
1405
|
-
clearTimeout(_fetchTimeoutHandle);
|
|
1406
|
-
}
|
|
1407
|
-
// Perform initial grid maintenance (thresholds, divergence, spread, health)
|
|
1408
|
-
// Consolidated into shared logic to ensure consistent behavior at boot and runtime.
|
|
1409
|
-
// CRITICAL: Pass lockAlreadyHeld since we're inside _fillProcessingLock.acquire()
|
|
1410
|
-
await this._runGridMaintenance('startup');
|
|
1411
|
-
// Cancel any dust from a previous bot lifetime immediately.
|
|
1412
|
-
const startupHealth = await this.manager.checkGridHealth(this.updateOrdersOnChainPlan.bind(this));
|
|
1413
|
-
await this._cancelDustOrders({
|
|
1414
|
-
buy: startupHealth.buyDustOrders,
|
|
1415
|
-
sell: startupHealth.sellDustOrders,
|
|
1416
|
-
});
|
|
1417
|
-
this._log('Bootstrap phase complete - fill processing resumed', 'info');
|
|
1418
|
-
}
|
|
1419
|
-
finally {
|
|
1420
|
-
// CRITICAL: Always clear bootstrap flag, even on error
|
|
1421
|
-
this.manager.finishBootstrap();
|
|
1422
|
-
}
|
|
1423
|
-
});
|
|
1424
|
-
await this._setupTriggerFileDetection();
|
|
1425
|
-
await this._setupCreditRuntime();
|
|
1426
|
-
await this._refreshAndSyncCreditRuntime();
|
|
1427
|
-
await this._runCreditRuntimeMaintenance('startup');
|
|
1428
|
-
this._setupBlockchainFetchInterval();
|
|
1429
|
-
this._setupCreditWatchdogInterval();
|
|
1430
|
-
this._setupCredentialDaemonWatchdogInterval();
|
|
1431
|
-
// Periodic dust health check — catches partials that fell below threshold
|
|
1432
|
-
// without triggering the post-fill pipeline. Cancels immediately inside the
|
|
1433
|
-
// fill-processing lock to avoid racing with fill batches or sync operations.
|
|
1434
|
-
this._setupDustHealthCheckInterval();
|
|
1435
|
-
await this._runDustHealthCheck();
|
|
1436
|
-
this._log('[DUST] Startup health check complete');
|
|
1437
|
-
if (this._isOpenOrdersSyncLoopEnabled()) {
|
|
1438
|
-
this._startOpenOrdersSyncLoop();
|
|
1439
|
-
}
|
|
1440
|
-
else {
|
|
1441
|
-
this._log('Open-orders sync loop disabled by configuration (TIMING.OPEN_ORDERS_SYNC_LOOP_ENABLED=false)');
|
|
1442
|
-
}
|
|
1443
|
-
this._log(`DEXBot started. OrderManager running (dryRun=${!!this.config.dryRun})`);
|
|
1444
|
-
}
|
|
1445
|
-
catch (err) {
|
|
1446
|
-
this._warn(`Error during grid initialization: ${err.message}`);
|
|
1447
|
-
await this.shutdown();
|
|
1448
|
-
throw err;
|
|
1449
|
-
}
|
|
628
|
+
return DexbotStartupRuntime.finishStartupSequence(this, startupState);
|
|
1450
629
|
}
|
|
1451
630
|
/**
|
|
1452
631
|
* Create the fill callback for listenForFills.
|
|
@@ -1465,35 +644,10 @@ class DEXBot {
|
|
|
1465
644
|
* @returns {Promise<{syncResult: Object|null, aborted: boolean, hasUnmatched: number, openOrders: Array|null}>}
|
|
1466
645
|
*/
|
|
1467
646
|
async _syncOpenOrdersAndProcessFills(tag) {
|
|
1468
|
-
|
|
1469
|
-
return { syncResult: null, aborted: false, hasUnmatched: 0, openOrders: null };
|
|
1470
|
-
}
|
|
1471
|
-
try {
|
|
1472
|
-
let openOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
1473
|
-
const syncResult = await this.manager.synchronizeWithChain(openOrders, 'readOpenOrders');
|
|
1474
|
-
let aborted = false;
|
|
1475
|
-
if (syncResult?.filledOrders?.length > 0) {
|
|
1476
|
-
this._refreshDynamicWeightDistribution(`${tag} sync-fill`);
|
|
1477
|
-
this._log(`[SYNC-CHAIN] ${syncResult.filledOrders.length} filled order(s) found during ${tag}`, 'info');
|
|
1478
|
-
const batchResult = await this._processFillsWithBatching(syncResult.filledOrders, new Set(), `${tag} sync-fill`);
|
|
1479
|
-
if (!batchResult?.aborted) {
|
|
1480
|
-
openOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
1481
|
-
await this.manager.synchronizeWithChain(openOrders, 'readOpenOrders');
|
|
1482
|
-
}
|
|
1483
|
-
else {
|
|
1484
|
-
aborted = true;
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
const hasUnmatched = syncResult?.unmatchedChainOrders?.length || 0;
|
|
1488
|
-
return { syncResult, aborted, hasUnmatched, openOrders };
|
|
1489
|
-
}
|
|
1490
|
-
catch (err) {
|
|
1491
|
-
this._warn(`[SYNC-CHAIN] Open-orders sync failed during ${tag}: ${err.message}`);
|
|
1492
|
-
return { syncResult: null, aborted: true, hasUnmatched: -1, openOrders: null };
|
|
1493
|
-
}
|
|
647
|
+
return dexbot_maintenance_runtime_1.default.syncOpenOrdersAndProcessFills(this, tag);
|
|
1494
648
|
}
|
|
1495
649
|
_maxConsecutiveFillConsumerFailures() {
|
|
1496
|
-
return this
|
|
650
|
+
return DexbotFillRuntime.maxConsecutiveFillConsumerFailures(this);
|
|
1497
651
|
}
|
|
1498
652
|
/**
|
|
1499
653
|
* Compute the backoff delay for fill-consumer retries after the failure
|
|
@@ -1505,80 +659,10 @@ class DEXBot {
|
|
|
1505
659
|
* @private
|
|
1506
660
|
*/
|
|
1507
661
|
_computeFillConsumerBackoffMs(failures) {
|
|
1508
|
-
|
|
1509
|
-
const initial = fp.CONSUMER_BACKOFF_INITIAL_MS;
|
|
1510
|
-
const max = fp.CONSUMER_BACKOFF_MAX_MS;
|
|
1511
|
-
const stepAfterMax = Math.max(0, failures - this._maxConsecutiveFillConsumerFailures());
|
|
1512
|
-
// 0 -> initial, 1 -> 2*initial, 2 -> 4*initial, ... capped at max.
|
|
1513
|
-
return Math.min(max, initial * Math.pow(2, stepAfterMax));
|
|
662
|
+
return DexbotFillRuntime.computeFillConsumerBackoffMs(this, failures);
|
|
1514
663
|
}
|
|
1515
664
|
_scheduleFillConsumerRestart(chainOrders) {
|
|
1516
|
-
|
|
1517
|
-
if (failures >= this._maxConsecutiveFillConsumerFailures()) {
|
|
1518
|
-
// Past the failure budget: switch from tight setImmediate loop to
|
|
1519
|
-
// exponential backoff. The consumer continues to retry — a transient
|
|
1520
|
-
// outage (e.g., credential daemon recovery) will resume normal
|
|
1521
|
-
// operation as soon as one cycle succeeds and resets the counter.
|
|
1522
|
-
// A permanent failure mode yields slower-but-still-progressing
|
|
1523
|
-
// retries capped at CONSUMER_BACKOFF_MAX_MS, with no permanent
|
|
1524
|
-
// stop that would require a bot restart.
|
|
1525
|
-
const backoffMs = this._computeFillConsumerBackoffMs(failures);
|
|
1526
|
-
const elapsedSec = this._consumeFailureFirstAt
|
|
1527
|
-
? Math.round((Date.now() - this._consumeFailureFirstAt) / TIMING.MILLISECONDS_PER_SECOND)
|
|
1528
|
-
: null;
|
|
1529
|
-
const elapsed = elapsedSec !== null ? `${elapsedSec}s` : 'unknown';
|
|
1530
|
-
// Escalate log level on sustained failure so operators monitoring
|
|
1531
|
-
// for error/critical alerts are not blind to a stuck consumer.
|
|
1532
|
-
// - warn : within the first escalation window (5-9 failures,
|
|
1533
|
-
// <5 min) — could be a slow recovery
|
|
1534
|
-
// - error : 10+ failures OR 5+ minutes of sustained failure
|
|
1535
|
-
// - critical: 20+ failures OR 15+ minutes of sustained failure —
|
|
1536
|
-
// this is the "permanent fault" signal
|
|
1537
|
-
const sustainedLevel = (failures >= 20 || (elapsedSec !== null && elapsedSec >= 900))
|
|
1538
|
-
? 'critical'
|
|
1539
|
-
: (failures >= 10 || (elapsedSec !== null && elapsedSec >= 300))
|
|
1540
|
-
? 'error'
|
|
1541
|
-
: 'warn';
|
|
1542
|
-
this._log(`[FILL-QUEUE] Fill consumer has failed ${failures} consecutive times over ${elapsed}; ` +
|
|
1543
|
-
`backing off ${Math.round(backoffMs / TIMING.MILLISECONDS_PER_SECOND)}s before retry. ` +
|
|
1544
|
-
`Queue: ${this._incomingFillQueue.length} fills.`, sustainedLevel);
|
|
1545
|
-
setTimeout(() => {
|
|
1546
|
-
if (this._shuttingDown)
|
|
1547
|
-
return;
|
|
1548
|
-
this._consumeFillQueue(chainOrders).catch(err => {
|
|
1549
|
-
if (!this._consumeFailureFirstAt) {
|
|
1550
|
-
this._consumeFailureFirstAt = Date.now();
|
|
1551
|
-
}
|
|
1552
|
-
this._consecutiveConsumeFailures++;
|
|
1553
|
-
const newFailures = this._consecutiveConsumeFailures;
|
|
1554
|
-
const newElapsedSec = this._consumeFailureFirstAt
|
|
1555
|
-
? Math.round((Date.now() - this._consumeFailureFirstAt) / TIMING.MILLISECONDS_PER_SECOND)
|
|
1556
|
-
: null;
|
|
1557
|
-
const resumeLevel = (newFailures >= 20 || (newElapsedSec !== null && newElapsedSec >= 900))
|
|
1558
|
-
? 'critical'
|
|
1559
|
-
: (newFailures >= 10 || (newElapsedSec !== null && newElapsedSec >= 300))
|
|
1560
|
-
? 'error'
|
|
1561
|
-
: 'warn';
|
|
1562
|
-
this._log(`Fill consumer resume after backoff failed ` +
|
|
1563
|
-
`(${newFailures} total, ` +
|
|
1564
|
-
`next backoff ${Math.round(this._computeFillConsumerBackoffMs(newFailures) / TIMING.MILLISECONDS_PER_SECOND)}s): ` +
|
|
1565
|
-
`${err.message}`, resumeLevel);
|
|
1566
|
-
// Continue the backoff loop. The success path of
|
|
1567
|
-
// _consumeFillQueue resets the counter, breaking the cycle.
|
|
1568
|
-
this._scheduleFillConsumerRestart(chainOrders);
|
|
1569
|
-
});
|
|
1570
|
-
}, backoffMs);
|
|
1571
|
-
return;
|
|
1572
|
-
}
|
|
1573
|
-
setImmediate(() => this._consumeFillQueue(chainOrders).catch(err => {
|
|
1574
|
-
if (!this._consumeFailureFirstAt) {
|
|
1575
|
-
this._consumeFailureFirstAt = Date.now();
|
|
1576
|
-
}
|
|
1577
|
-
this._consecutiveConsumeFailures++;
|
|
1578
|
-
const remaining = this._maxConsecutiveFillConsumerFailures() - this._consecutiveConsumeFailures;
|
|
1579
|
-
this._log(`Fill consumer failed (${this._consecutiveConsumeFailures}/${this._maxConsecutiveFillConsumerFailures()}, ` +
|
|
1580
|
-
`${remaining} attempts remaining): ${err.message}`, this._consecutiveConsumeFailures >= 3 ? 'warn' : 'error');
|
|
1581
|
-
}));
|
|
665
|
+
DexbotFillRuntime.scheduleFillConsumerRestart(this, chainOrders);
|
|
1582
666
|
}
|
|
1583
667
|
/**
|
|
1584
668
|
* Consume queued fills from incomingFillQueue and rebalance.
|
|
@@ -1594,564 +678,7 @@ class DEXBot {
|
|
|
1594
678
|
* @private
|
|
1595
679
|
*/
|
|
1596
680
|
async _consumeFillQueue(chainOrders) {
|
|
1597
|
-
|
|
1598
|
-
// The counter only tracks actual failures, so any healthy deferral
|
|
1599
|
-
// path should also reset the counter. Without this, a sequence of
|
|
1600
|
-
// F-S-F-S-F-S-F-S-F (fail, succeed, fail, succeed, ...) would still
|
|
1601
|
-
// reach the max and trip backoff, OR a failure followed by an empty
|
|
1602
|
-
// queue / shutdown / in-flight batch would leave the counter sticky
|
|
1603
|
-
// and trigger backoff one step sooner on the next real failure.
|
|
1604
|
-
const resetFailureWatchdogIfSet = () => {
|
|
1605
|
-
if (this._consecutiveConsumeFailures > 0 || this._consumeFailureFirstAt > 0) {
|
|
1606
|
-
this._consecutiveConsumeFailures = 0;
|
|
1607
|
-
this._consumeFailureFirstAt = 0;
|
|
1608
|
-
}
|
|
1609
|
-
};
|
|
1610
|
-
// ATOMIC: Only attempt lock acquisition if queue has work
|
|
1611
|
-
// This prevents unnecessary lock contention on empty queues
|
|
1612
|
-
if (this._incomingFillQueue.length === 0) {
|
|
1613
|
-
// Empty queue = consumer is healthy, just idle.
|
|
1614
|
-
resetFailureWatchdogIfSet();
|
|
1615
|
-
return;
|
|
1616
|
-
}
|
|
1617
|
-
// Check shutdown state
|
|
1618
|
-
if (this._shuttingDown) {
|
|
1619
|
-
this._warn('Fill processing skipped: shutdown in progress');
|
|
1620
|
-
resetFailureWatchdogIfSet();
|
|
1621
|
-
return;
|
|
1622
|
-
}
|
|
1623
|
-
if (this._batchInFlight || this._recoverySyncInFlight) {
|
|
1624
|
-
this.manager?.logger?.log?.(`Fill processing deferred: order pipeline active (${this._incomingFillQueue.length} queued)`, 'debug');
|
|
1625
|
-
// A batch is in flight, not a failure. The next iteration will
|
|
1626
|
-
// either succeed (and reset the counter on the success path) or
|
|
1627
|
-
// fail and increment it. Either way, leaving an old failure
|
|
1628
|
-
// count here would double-count.
|
|
1629
|
-
resetFailureWatchdogIfSet();
|
|
1630
|
-
return;
|
|
1631
|
-
}
|
|
1632
|
-
let pendingFillKeysForCurrentCycle = new Set();
|
|
1633
|
-
try {
|
|
1634
|
-
// BOOTSTRAP OPTIMIZATION: During bootstrap, prioritize fill processing over grid-wide checks
|
|
1635
|
-
// Process fills immediately with side-only rebalancing (no expensive full grid recalculations)
|
|
1636
|
-
if (this.manager.isBootstrapping()) {
|
|
1637
|
-
// During bootstrap: skip lock contention checks, process fills directly
|
|
1638
|
-
let bootstrapSkipped = false;
|
|
1639
|
-
await this.manager._fillProcessingLock.acquire(async () => {
|
|
1640
|
-
if (!this.manager.isBootstrapping()) {
|
|
1641
|
-
// Bootstrap finished while waiting for the lock — no
|
|
1642
|
-
// work to do, but the iteration is still healthy.
|
|
1643
|
-
bootstrapSkipped = true;
|
|
1644
|
-
return;
|
|
1645
|
-
}
|
|
1646
|
-
await this._processFillsWithBootstrapMode(chainOrders);
|
|
1647
|
-
});
|
|
1648
|
-
if (bootstrapSkipped) {
|
|
1649
|
-
// Bootstrap-mode lock callback returned without doing
|
|
1650
|
-
// work; the .acquire() success path at line ~1941 that
|
|
1651
|
-
// would normally reset the counter is not reached in
|
|
1652
|
-
// this branch. Reset here so a stale counter from a
|
|
1653
|
-
// prior failure doesn't carry over.
|
|
1654
|
-
resetFailureWatchdogIfSet();
|
|
1655
|
-
}
|
|
1656
|
-
return;
|
|
1657
|
-
}
|
|
1658
|
-
// NORMAL MODE: Non-blocking check if lock already has waiters
|
|
1659
|
-
// This prevents unbounded queue growth while still ensuring processing
|
|
1660
|
-
// Note: We DO proceed if lock is held but has no waiters - we'll wait our turn
|
|
1661
|
-
if (this.manager._fillProcessingLock.getQueueLength() > 0) {
|
|
1662
|
-
this._metrics.lockContentionEvents++;
|
|
1663
|
-
// Deferral, not a failure. Reset the watchdog so the next
|
|
1664
|
-
// call (which may now find an empty queue, or process
|
|
1665
|
-
// successfully) doesn't inherit a stale counter.
|
|
1666
|
-
resetFailureWatchdogIfSet();
|
|
1667
|
-
return;
|
|
1668
|
-
}
|
|
1669
|
-
await this.manager._fillProcessingLock.acquire(async () => {
|
|
1670
|
-
// Reset orphan-fill credit timestamp at the start of each
|
|
1671
|
-
// fill cycle. It is re-set when orphan fills are credited,
|
|
1672
|
-
// and consumed (set to null) on the next fund-invariant check
|
|
1673
|
-
// in accounting.ts, which widens tolerance by 5x while set.
|
|
1674
|
-
// Written to this.manager (not this) because accounting.ts
|
|
1675
|
-
// reads from the OrderManager reference (mgr).
|
|
1676
|
-
// Also cleared by _performStateRecovery after a fresh chain
|
|
1677
|
-
// fetch. The timestamp value itself is not compared against a
|
|
1678
|
-
// window — it acts as a consume-on-read boolean.
|
|
1679
|
-
this.manager._orphanFillsCreditedAt = null;
|
|
1680
|
-
while (this._incomingFillQueue.length > 0) {
|
|
1681
|
-
const batchStartTime = Date.now();
|
|
1682
|
-
// Track max queue depth
|
|
1683
|
-
this._metrics.maxQueueDepth = Math.max(this._metrics.maxQueueDepth, this._incomingFillQueue.length);
|
|
1684
|
-
// 1. Take snapshot of current work (ATOMIC: splice removes and returns fills atomically)
|
|
1685
|
-
const allFills = this._incomingFillQueue.splice(0); // Atomically clear and get all fills
|
|
1686
|
-
const validFills = [];
|
|
1687
|
-
const processedFillKeys = new Set();
|
|
1688
|
-
pendingFillKeysForCurrentCycle = new Set();
|
|
1689
|
-
let requiresOpenOrdersSync = false;
|
|
1690
|
-
// 2. Filter and Deduplicate (Standard Logic)
|
|
1691
|
-
for (const fill of allFills) {
|
|
1692
|
-
if (fill && fill.op && fill.op[0] === FILL_PROCESSING.OPERATION_TYPE) {
|
|
1693
|
-
const fillOp = fill.op[1];
|
|
1694
|
-
// SELF-CANCEL GUARD: only drop malformed, cancel-like
|
|
1695
|
-
// artifacts for an order the local process just cancelled.
|
|
1696
|
-
// Real fill_order ops carry economic data and must still be
|
|
1697
|
-
// accounted even if they arrive shortly after a successful
|
|
1698
|
-
// cancel broadcast.
|
|
1699
|
-
const hasFillEconomics = fillOp?.pays?.asset_id && fillOp?.pays?.amount != null
|
|
1700
|
-
&& fillOp?.receives?.asset_id && fillOp?.receives?.amount != null;
|
|
1701
|
-
if (chainOrders && typeof chainOrders.wasRecentlyOwnCancelled === 'function'
|
|
1702
|
-
&& chainOrders.wasRecentlyOwnCancelled(fillOp.order_id)
|
|
1703
|
-
&& !hasFillEconomics) {
|
|
1704
|
-
this.manager.logger.log(`[SELF-CANCEL] Skipping non-economic fill artifact for order ${fillOp.order_id} (just cancelled by this bot)`, 'debug');
|
|
1705
|
-
continue;
|
|
1706
|
-
}
|
|
1707
|
-
// ACCOUNT VALIDATION: Verify the filled order belongs to this bot's account/grid
|
|
1708
|
-
// Only process fills for orders we actually manage
|
|
1709
|
-
const gridOrder = this.manager.orders.get(fillOp.order_id) ||
|
|
1710
|
-
Array.from(this.manager.orders.values()).find((o) => o.orderId === fillOp.order_id);
|
|
1711
|
-
if (!gridOrder) {
|
|
1712
|
-
// Check if this order was already freed by stale-order batch cleanup.
|
|
1713
|
-
// When a batch fails due to a stale order reference, the cleanup converts the
|
|
1714
|
-
// slot to VIRTUAL/SPREAD, releasing committed funds to chainFree. If we also
|
|
1715
|
-
// credit the fill proceeds here, we double-count the capital.
|
|
1716
|
-
const staleMarkedAt = this._staleCleanedOrderIds.get(fillOp.order_id);
|
|
1717
|
-
if (staleMarkedAt != null) {
|
|
1718
|
-
const staleAgeMs = Date.now() - staleMarkedAt;
|
|
1719
|
-
if (staleAgeMs <= this._staleCleanupRetentionMs) {
|
|
1720
|
-
this.manager.logger.log(`[ORPHAN-FILL] Skipping double-credit for stale-cleaned order ${fillOp.order_id} ` +
|
|
1721
|
-
`(funds already freed by batch cleanup, age=${staleAgeMs}ms)`, 'warn');
|
|
1722
|
-
continue;
|
|
1723
|
-
}
|
|
1724
|
-
this._staleCleanedOrderIds.delete(fillOp.order_id);
|
|
1725
|
-
}
|
|
1726
|
-
// Legitimate orphan fill: order was virtualized during sequential processing
|
|
1727
|
-
// but a fill arrived afterward. Credit proceeds to maintain fund tracking.
|
|
1728
|
-
let orphanFillKey = buildFillKey(fill);
|
|
1729
|
-
if (!orphanFillKey) {
|
|
1730
|
-
orphanFillKey = this._buildOrphanFillFallbackKey(fill);
|
|
1731
|
-
}
|
|
1732
|
-
if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[ORPHAN-FILL]', fillOp.order_id)) {
|
|
1733
|
-
continue;
|
|
1734
|
-
}
|
|
1735
|
-
this.manager.logger.log(`[ORPHAN-FILL] Processing funds for unknown order ${fillOp.order_id} (not in grid but crediting proceeds)`, 'warn');
|
|
1736
|
-
const accountingResult = await this._applyReplaySafeOrphanFillAccounting(fill, fillOp, {
|
|
1737
|
-
context: 'ORPHAN-FILL',
|
|
1738
|
-
replayMessage: (op) => `[ORPHAN-FILL] Replay detected for ${op.order_id}; skipping duplicate credit`
|
|
1739
|
-
});
|
|
1740
|
-
if (accountingResult.status === 'missing_key') {
|
|
1741
|
-
requiresOpenOrdersSync = true;
|
|
1742
|
-
}
|
|
1743
|
-
// Record orphan fill credit timestamp for fund invariant
|
|
1744
|
-
// tolerance widening. Written to this.manager because
|
|
1745
|
-
// accounting.ts reads from the OrderManager (mgr).
|
|
1746
|
-
this.manager._orphanFillsCreditedAt = Date.now();
|
|
1747
|
-
// Don't add to validFills - we can't do rebalancing without a grid slot
|
|
1748
|
-
// But the funds are now credited, preventing fund invariant violation
|
|
1749
|
-
continue;
|
|
1750
|
-
}
|
|
1751
|
-
// Process both maker and taker fills for our grid orders
|
|
1752
|
-
// Grid validation ensures we only process fills belonging to our account
|
|
1753
|
-
// Taker fills are included because the bot may execute market orders or act as taker
|
|
1754
|
-
const roleStr = fillOp.is_maker !== false ? 'maker' : 'taker';
|
|
1755
|
-
this.manager.logger.log(`Processing ${roleStr} fill for order ${fillOp.order_id}`, 'debug');
|
|
1756
|
-
const fillKey = buildFillKey(fill);
|
|
1757
|
-
if (!fillKey) {
|
|
1758
|
-
this.manager.logger.log(`[FILL] Missing history id for order ${fillOp.order_id} block ${fill.block_num}; deferring to open-orders sync`, 'warn');
|
|
1759
|
-
requiresOpenOrdersSync = true;
|
|
1760
|
-
continue;
|
|
1761
|
-
}
|
|
1762
|
-
if (!this._isNewFillKey(fillKey, processedFillKeys, '[FILL]', fillOp.order_id)) {
|
|
1763
|
-
continue;
|
|
1764
|
-
}
|
|
1765
|
-
validFills.push(fill);
|
|
1766
|
-
// Log info
|
|
1767
|
-
const paysAmount = fillOp.pays ? fillOp.pays.amount : '?';
|
|
1768
|
-
const receivesAmount = fillOp.receives ? fillOp.receives.amount : '?';
|
|
1769
|
-
this._log(`\n===== FILL DETECTED =====`);
|
|
1770
|
-
this._log(`Order ID: ${fillOp.order_id}`);
|
|
1771
|
-
this._log(`Pays: ${paysAmount}, Receives: ${receivesAmount}`);
|
|
1772
|
-
this._log(`Block: ${fill.block_num} (History ID: ${fill.id || 'N/A'})`);
|
|
1773
|
-
this._log(`=========================\n`);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
// Clean up short-lived queue dedupe cache to prevent memory leak.
|
|
1777
|
-
const cleanupTimestamp = Date.now();
|
|
1778
|
-
let cleanedCount = 0;
|
|
1779
|
-
for (const [key, timestamp] of this._recentlyQueuedFills) {
|
|
1780
|
-
if (cleanupTimestamp - timestamp > this._fillDedupeWindowMs) {
|
|
1781
|
-
this._recentlyQueuedFills.delete(key);
|
|
1782
|
-
cleanedCount++;
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
if (cleanedCount > 0) {
|
|
1786
|
-
this.manager.logger.log(`Cleaned ${cleanedCount} old queued fill records. Remaining: ${this._recentlyQueuedFills.size}`, 'debug');
|
|
1787
|
-
}
|
|
1788
|
-
if (validFills.length === 0 && !requiresOpenOrdersSync)
|
|
1789
|
-
continue; // Loop back for more
|
|
1790
|
-
// 3. Sync and Collect Filled Orders
|
|
1791
|
-
let allFilledOrders = [];
|
|
1792
|
-
let ordersNeedingCorrection = [];
|
|
1793
|
-
const pendingGhostOrders = new Set();
|
|
1794
|
-
const fillMode = chainOrders.getFillProcessingMode();
|
|
1795
|
-
const processValidFills = async (fillsToSync) => {
|
|
1796
|
-
let resolvedOrders = [];
|
|
1797
|
-
if (fillMode === 'history') {
|
|
1798
|
-
this.manager.logger.log(`Syncing ${fillsToSync.length} fill(s) (history mode)`, 'info');
|
|
1799
|
-
// Batch mode for 2+ fills: acquires _gridLock once, batches drift refetch
|
|
1800
|
-
if (fillsToSync.length >= 2) {
|
|
1801
|
-
const batchResult = await this.manager.syncFromFillHistoryBatch(fillsToSync, {
|
|
1802
|
-
persistenceMode: PROCESSED_FILL_PERSISTENCE_MODES.BATCHED
|
|
1803
|
-
});
|
|
1804
|
-
for (const fill of fillsToSync) {
|
|
1805
|
-
const fillKey = buildFillKey({
|
|
1806
|
-
orderId: fill?.op?.[1]?.order_id,
|
|
1807
|
-
blockNum: fill?.block_num,
|
|
1808
|
-
historyId: fill?.id
|
|
1809
|
-
});
|
|
1810
|
-
if (fillKey)
|
|
1811
|
-
pendingFillKeysForCurrentCycle.add(fillKey);
|
|
1812
|
-
}
|
|
1813
|
-
if (batchResult.filledOrders)
|
|
1814
|
-
resolvedOrders.push(...batchResult.filledOrders);
|
|
1815
|
-
if (batchResult.requiresOpenOrdersSync)
|
|
1816
|
-
requiresOpenOrdersSync = true;
|
|
1817
|
-
if (batchResult.ghostOrderIds?.length > 0) {
|
|
1818
|
-
for (const id of batchResult.ghostOrderIds) {
|
|
1819
|
-
pendingGhostOrders.add(id);
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
else {
|
|
1824
|
-
// Single fill: use individual per-fill path
|
|
1825
|
-
for (const fill of fillsToSync) {
|
|
1826
|
-
const resultHistory = await this.manager.syncFromFillHistory(fill, {
|
|
1827
|
-
persistenceMode: PROCESSED_FILL_PERSISTENCE_MODES.BATCHED
|
|
1828
|
-
});
|
|
1829
|
-
const fillKey = buildFillKey({
|
|
1830
|
-
orderId: fill?.op?.[1]?.order_id,
|
|
1831
|
-
blockNum: fill?.block_num,
|
|
1832
|
-
historyId: fill?.id
|
|
1833
|
-
});
|
|
1834
|
-
if (fillKey)
|
|
1835
|
-
pendingFillKeysForCurrentCycle.add(fillKey);
|
|
1836
|
-
// Dust is handled by post-fill detection below.
|
|
1837
|
-
if (resultHistory.filledOrders)
|
|
1838
|
-
resolvedOrders.push(...resultHistory.filledOrders);
|
|
1839
|
-
if (resultHistory.requiresOpenOrdersSync)
|
|
1840
|
-
requiresOpenOrdersSync = true;
|
|
1841
|
-
if (resultHistory.ghostOrderId)
|
|
1842
|
-
pendingGhostOrders.add(resultHistory.ghostOrderId);
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
if (fillMode !== 'history' || requiresOpenOrdersSync) {
|
|
1847
|
-
if (fillMode === 'history' && requiresOpenOrdersSync) {
|
|
1848
|
-
this.manager.logger.log('Falling back to open-orders sync for fill(s) missing replay-safe history identifiers', 'warn');
|
|
1849
|
-
}
|
|
1850
|
-
this.manager.logger.log(`Syncing ${fillsToSync.length} fill(s) (open orders mode)`, 'info');
|
|
1851
|
-
const chainOpenOrders = await chainOrders.readOpenOrders(this.account);
|
|
1852
|
-
const resultOpenOrders = await this.manager.syncFromOpenOrders(chainOpenOrders);
|
|
1853
|
-
// Dust is handled by post-fill detection below.
|
|
1854
|
-
if (resultOpenOrders.filledOrders)
|
|
1855
|
-
resolvedOrders.push(...resultOpenOrders.filledOrders);
|
|
1856
|
-
if (resultOpenOrders.ordersNeedingCorrection)
|
|
1857
|
-
ordersNeedingCorrection.push(...resultOpenOrders.ordersNeedingCorrection);
|
|
1858
|
-
}
|
|
1859
|
-
return resolvedOrders;
|
|
1860
|
-
};
|
|
1861
|
-
this.manager.pauseFundRecalc();
|
|
1862
|
-
try {
|
|
1863
|
-
// FIX 1: Block-level fill batching — group valid fills by block
|
|
1864
|
-
// and process each block group as a unit. This prevents slot
|
|
1865
|
-
// collisions when fills from the same block (original + replacement
|
|
1866
|
-
// orders both filled) arrive in different sync batches. Processing
|
|
1867
|
-
// all fills from a block together lets the sync engine see every
|
|
1868
|
-
// fill for overlapping slots simultaneously.
|
|
1869
|
-
const fillsByBlock = new Map();
|
|
1870
|
-
const fillsWithoutBlock = [];
|
|
1871
|
-
for (const fill of validFills) {
|
|
1872
|
-
if (fill.block_num != null) {
|
|
1873
|
-
const list = fillsByBlock.get(fill.block_num);
|
|
1874
|
-
if (list)
|
|
1875
|
-
list.push(fill);
|
|
1876
|
-
else
|
|
1877
|
-
fillsByBlock.set(fill.block_num, [fill]);
|
|
1878
|
-
}
|
|
1879
|
-
else {
|
|
1880
|
-
fillsWithoutBlock.push(fill);
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
// Process block groups in ascending block order so the sync
|
|
1884
|
-
// engine sees a deterministic, chronological fill sequence.
|
|
1885
|
-
const sortedBlocks = [...fillsByBlock.keys()].sort((a, b) => a - b);
|
|
1886
|
-
const accumulatedOrders = [];
|
|
1887
|
-
let anyRequiresSync = false;
|
|
1888
|
-
// Capture whether any fill was skipped during validation
|
|
1889
|
-
// (e.g. missing history ID) and set the sync flag. The
|
|
1890
|
-
// per-block reset below would lose this state since those
|
|
1891
|
-
// fills were filtered out of validFills and never enter
|
|
1892
|
-
// any block group.
|
|
1893
|
-
const initialRequiresSync = requiresOpenOrdersSync;
|
|
1894
|
-
for (const blockNum of sortedBlocks) {
|
|
1895
|
-
// Reset requiresOpenOrdersSync per block group so one
|
|
1896
|
-
// block's history-id gap doesn't force the next block
|
|
1897
|
-
// into an unnecessary open-orders snapshot re-fetch.
|
|
1898
|
-
requiresOpenOrdersSync = false;
|
|
1899
|
-
this.manager.logger.log(`[FILL-BLOCK] Processing ${fillsByBlock.get(blockNum).length} fill(s) from block ${blockNum}`, 'debug');
|
|
1900
|
-
const blockResult = await processValidFills(fillsByBlock.get(blockNum));
|
|
1901
|
-
accumulatedOrders.push(...blockResult);
|
|
1902
|
-
if (requiresOpenOrdersSync)
|
|
1903
|
-
anyRequiresSync = true;
|
|
1904
|
-
}
|
|
1905
|
-
// Preserve both per-block sync flags and the initial flag
|
|
1906
|
-
// from filtered-out fills (missing history ID).
|
|
1907
|
-
requiresOpenOrdersSync = anyRequiresSync || initialRequiresSync;
|
|
1908
|
-
if (fillsWithoutBlock.length > 0) {
|
|
1909
|
-
this.manager.logger.log(`[FILL-BLOCK] Processing ${fillsWithoutBlock.length} fill(s) without block info`, 'debug');
|
|
1910
|
-
const noBlockResult = await processValidFills(fillsWithoutBlock);
|
|
1911
|
-
accumulatedOrders.push(...noBlockResult);
|
|
1912
|
-
if (requiresOpenOrdersSync)
|
|
1913
|
-
anyRequiresSync = true;
|
|
1914
|
-
}
|
|
1915
|
-
// If a fill was filtered out during validation (e.g. missing
|
|
1916
|
-
// history ID) and no block group triggered the open-orders
|
|
1917
|
-
// sync, run it now so the grid state is reconciled.
|
|
1918
|
-
if (requiresOpenOrdersSync && !anyRequiresSync) {
|
|
1919
|
-
this.manager.logger.log('[FILL-BLOCK] Running open-orders sync for fills with missing history identifiers', 'warn');
|
|
1920
|
-
const fallbackOrders = await processValidFills([]);
|
|
1921
|
-
accumulatedOrders.push(...fallbackOrders);
|
|
1922
|
-
// Flag consumed — variable goes out of scope on
|
|
1923
|
-
// next while iteration.
|
|
1924
|
-
}
|
|
1925
|
-
allFilledOrders = accumulatedOrders;
|
|
1926
|
-
// 4. Handle Price Corrections
|
|
1927
|
-
if (ordersNeedingCorrection.length > 0) {
|
|
1928
|
-
const correctionResult = await correctAllPriceMismatches(this.manager, this.account, this.privateKey, chainOrders);
|
|
1929
|
-
if (correctionResult.failed > 0)
|
|
1930
|
-
this.manager.logger.log(`${correctionResult.failed} corrections failed`, 'error');
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
finally {
|
|
1934
|
-
// 4b. Cancel orphaned chain orders detected by other-side rounding.
|
|
1935
|
-
// These orders have a tiny residual that the bot treated as fully
|
|
1936
|
-
// filled, but the blockchain did not close the order. We send a
|
|
1937
|
-
// cancel tx to clean up the zombie order on chain.
|
|
1938
|
-
// Runs in the finally so it still fires even if processValidFills
|
|
1939
|
-
// throws after collecting ghost IDs into pendingGhostOrders.
|
|
1940
|
-
//
|
|
1941
|
-
// OPTIMIZATION: Batch all new ghost cancels into one executeBatch
|
|
1942
|
-
// call with multiple cancelOrder ops, reducing per-order tx fees
|
|
1943
|
-
// and broadcast overhead. Falls back to individual cancels on
|
|
1944
|
-
// batch failure.
|
|
1945
|
-
if (pendingGhostOrders.size > 0) {
|
|
1946
|
-
if (!this._ghostOrderCancelAttempted)
|
|
1947
|
-
this._ghostOrderCancelAttempted = new Set();
|
|
1948
|
-
const newGhostIds = [...pendingGhostOrders].filter(id => !this._ghostOrderCancelAttempted.has(id));
|
|
1949
|
-
if (newGhostIds.length > 0) {
|
|
1950
|
-
const MAX_OPS_PER_TX = 200;
|
|
1951
|
-
let batchFailed = false;
|
|
1952
|
-
// Build cancel ops with per-ID error tolerance.
|
|
1953
|
-
// Index correlates 1:1 with newGhostIds (allSettled preserves order).
|
|
1954
|
-
const buildResults = await Promise.allSettled(newGhostIds.map(id => chainOrders.buildCancelOrderOp(this.account, id)));
|
|
1955
|
-
const cancelOps = [];
|
|
1956
|
-
for (let i = 0; i < buildResults.length; i++) {
|
|
1957
|
-
const result = buildResults[i];
|
|
1958
|
-
const id = newGhostIds[i];
|
|
1959
|
-
if (result.status === 'fulfilled') {
|
|
1960
|
-
cancelOps.push(result.value);
|
|
1961
|
-
}
|
|
1962
|
-
else {
|
|
1963
|
-
this.manager.logger.log(`[SYNC] Failed to build cancel op for ghost order ${id}: ${result.reason?.message || result.reason}`, 'warn');
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
// Chunk into batches respecting MAX_OPS_PER_TX
|
|
1967
|
-
for (let i = 0; i < cancelOps.length; i += MAX_OPS_PER_TX) {
|
|
1968
|
-
const chunk = cancelOps.slice(i, i + MAX_OPS_PER_TX);
|
|
1969
|
-
const batchIds = newGhostIds.slice(i, i + MAX_OPS_PER_TX);
|
|
1970
|
-
try {
|
|
1971
|
-
this.manager.logger.log(`[SYNC] Batch-cancelling ${chunk.length} orphaned chain order(s) ` +
|
|
1972
|
-
`(batch ${Math.floor(i / MAX_OPS_PER_TX) + 1}/${Math.ceil(cancelOps.length / MAX_OPS_PER_TX)})`, 'info');
|
|
1973
|
-
await chainOrders.executeBatch(this.account, this.privateKey, chunk);
|
|
1974
|
-
// Mark successfully cancelled IDs immediately
|
|
1975
|
-
for (const id of batchIds) {
|
|
1976
|
-
this._ghostOrderCancelAttempted.add(id);
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
catch (batchErr) {
|
|
1980
|
-
batchFailed = true;
|
|
1981
|
-
this.manager.logger.log(`[SYNC] Batch ghost cancel failed for chunk ${Math.floor(i / MAX_OPS_PER_TX) + 1} ` +
|
|
1982
|
-
`(${chunk.length} orders): ${batchErr?.message || batchErr}`, 'warn');
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
// Log overall success if all chunks and all builds succeeded
|
|
1986
|
-
if (!batchFailed && cancelOps.length === newGhostIds.length) {
|
|
1987
|
-
this.manager.logger.log(`[SYNC] Successfully batch-cancelled ${newGhostIds.length} orphaned chain order(s)`, 'info');
|
|
1988
|
-
}
|
|
1989
|
-
// Fallback: individual cancels for IDs that were not marked successful
|
|
1990
|
-
for (const ghostOrderId of newGhostIds) {
|
|
1991
|
-
if (this._ghostOrderCancelAttempted.has(ghostOrderId))
|
|
1992
|
-
continue;
|
|
1993
|
-
try {
|
|
1994
|
-
this.manager.logger.log(`[SYNC] Cancelling orphaned chain order ${ghostOrderId} (other-side full-fill residual)`, 'info');
|
|
1995
|
-
await chainOrders.cancelOrder(this.account, this.privateKey, ghostOrderId);
|
|
1996
|
-
this._ghostOrderCancelAttempted.add(ghostOrderId);
|
|
1997
|
-
}
|
|
1998
|
-
catch (err) {
|
|
1999
|
-
this.manager.logger.log(`[SYNC] Failed to cancel orphaned order ${ghostOrderId}: ${err?.message || err}`, 'warn');
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
await this.manager.resumeFundRecalc();
|
|
2005
|
-
}
|
|
2006
|
-
// Refresh dynamic weight distribution before processing fills
|
|
2007
|
-
// so the rebalance uses the latest market adapter weights, not
|
|
2008
|
-
// stale values from the last periodic refresh cycle.
|
|
2009
|
-
// Lightweight: reads local JSON snapshot file.
|
|
2010
|
-
this._refreshDynamicWeightDistribution('fill queue');
|
|
2011
|
-
// 5. Fixed-Cap Fill Rebalance
|
|
2012
|
-
// - 1..MAX_FILL_BATCH_SIZE fills: unified full-set planning
|
|
2013
|
-
// - larger bursts: fixed-size chunking at MAX_FILL_BATCH_SIZE
|
|
2014
|
-
if (allFilledOrders.length > 0) {
|
|
2015
|
-
const result = await this._processFillsWithBatching(allFilledOrders, null, 'fill set');
|
|
2016
|
-
let abortedFillCycle = result.aborted;
|
|
2017
|
-
if (!abortedFillCycle) {
|
|
2018
|
-
const batchFillKeys = new Set(allFilledOrders.map(filledOrder => buildFillKey({
|
|
2019
|
-
orderId: filledOrder?.orderId,
|
|
2020
|
-
blockNum: filledOrder?.blockNum,
|
|
2021
|
-
historyId: filledOrder?.historyId
|
|
2022
|
-
})).filter(Boolean));
|
|
2023
|
-
await this._flushProcessedFillPersistenceForKeys(batchFillKeys, 'fill-batch-committed');
|
|
2024
|
-
}
|
|
2025
|
-
else {
|
|
2026
|
-
this.manager.logger.log('[FILL-DEDUP] Fill cycle aborted; fill key persistence guarded under abort path.', 'warn');
|
|
2027
|
-
}
|
|
2028
|
-
// 6. Rebalance Recovery Loop (Sequential Extensions)
|
|
2029
|
-
// DISABLED FOR SEQUENTIAL: Each sequential fill already triggers a full rebalance with proper
|
|
2030
|
-
// boundary shift. An additional recovery loop with EMPTY fills causes the boundary to remain
|
|
2031
|
-
// at the last fill's position, leading to wrong operation types (updates instead of rotations)
|
|
2032
|
-
// and operations on the wrong side.
|
|
2033
|
-
//
|
|
2034
|
-
// In the future, recovery loop can be re-enabled for single fills if needed, but ONLY
|
|
2035
|
-
// if it passes the actual fills to processFilledOrders so the boundary shifts correctly.
|
|
2036
|
-
// For now: Each fill = full rebalance with boundary shift = complete correction in one pass.
|
|
2037
|
-
// CRITICAL: Do NOT run spread correction here during sequential fill processing.
|
|
2038
|
-
// The rebalance from each fill should maintain spread naturally. Running spread correction
|
|
2039
|
-
// immediately after creates new orders that may get filled by market before next cycle,
|
|
2040
|
-
// causing cascading fills and potentially SPREAD slots becoming PARTIAL (error condition).
|
|
2041
|
-
// Spread correction runs in the main loop instead.
|
|
2042
|
-
const fullFillCount = allFilledOrders.filter(o => o && o.isPartial !== true).length;
|
|
2043
|
-
const hasAnyFills = allFilledOrders.some(o => o);
|
|
2044
|
-
const shouldRunPostFillChecks = !abortedFillCycle && fullFillCount > 0;
|
|
2045
|
-
const shouldRunDustDetection = !abortedFillCycle && hasAnyFills;
|
|
2046
|
-
if (shouldRunDustDetection) {
|
|
2047
|
-
const healthResult = await this.manager.checkGridHealth(this.updateOrdersOnChainPlan.bind(this));
|
|
2048
|
-
const allDust = [
|
|
2049
|
-
...(healthResult.buyDustOrders || []),
|
|
2050
|
-
...(healthResult.sellDustOrders || []),
|
|
2051
|
-
];
|
|
2052
|
-
if (allDust.length > 0) {
|
|
2053
|
-
const dustCancelResult = await this._cancelDustOrders({
|
|
2054
|
-
buy: healthResult.buyDustOrders,
|
|
2055
|
-
sell: healthResult.sellDustOrders,
|
|
2056
|
-
});
|
|
2057
|
-
if (dustCancelResult?.batchResult?.aborted) {
|
|
2058
|
-
abortedFillCycle = true;
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
// Run grid maintenance after fills to rebuild degraded grid.
|
|
2063
|
-
// CRITICAL FIX (commit a946c33): Replaced inline divergence checks with centralized
|
|
2064
|
-
// _runGridMaintenance call to ensure pipeline protection applies consistently.
|
|
2065
|
-
// Before: Divergence checks ran immediately after fills, causing race-to-resize
|
|
2066
|
-
// After: Grid maintenance waits for isPipelineEmpty() before structural changes
|
|
2067
|
-
// Run only when the cycle contains at least one full fill.
|
|
2068
|
-
if (shouldRunPostFillChecks && !abortedFillCycle) {
|
|
2069
|
-
await this._runGridMaintenance('post-fill');
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
else if (pendingFillKeysForCurrentCycle.size > 0) {
|
|
2073
|
-
await this._flushProcessedFillPersistenceForKeys(pendingFillKeysForCurrentCycle, 'fill-batch-no-rotations');
|
|
2074
|
-
}
|
|
2075
|
-
this.manager._recentFillKeysSnapshot = this._getRecentFillKeysSnapshot();
|
|
2076
|
-
await retryPersistenceIfNeeded(this.manager);
|
|
2077
|
-
// Periodically clean up old fill records after processing N fills.
|
|
2078
|
-
// Counter is protected by _fillProcessingLock during fill consumption.
|
|
2079
|
-
this._fillCleanupCounter += validFills.length;
|
|
2080
|
-
const cleanupThreshold = MAINTENANCE.CLEANUP_PROBABILITY > 0 && MAINTENANCE.CLEANUP_PROBABILITY < 1
|
|
2081
|
-
? Math.floor(1 / MAINTENANCE.CLEANUP_PROBABILITY)
|
|
2082
|
-
: 100; // Default: every 100 fills
|
|
2083
|
-
if (this._fillCleanupCounter >= cleanupThreshold) {
|
|
2084
|
-
try {
|
|
2085
|
-
await this.accountOrders.cleanOldProcessedFills(TIMING.FILL_RECORD_RETENTION_MS);
|
|
2086
|
-
this._fillCleanupCounter = 0; // Reset counter after cleanup (success or retry on next batch if failed)
|
|
2087
|
-
}
|
|
2088
|
-
catch (err) {
|
|
2089
|
-
this.manager?.logger?.log(`Warning: Fill cleanup failed (will retry): ${err.message}`, 'warn');
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
// Update metrics
|
|
2093
|
-
this._metrics.fillsProcessed += validFills.length;
|
|
2094
|
-
this._metrics.fillProcessingTimeMs += Date.now() - batchStartTime;
|
|
2095
|
-
// Prune expired stale-cleaned order IDs after each processing cycle.
|
|
2096
|
-
if (this._staleCleanedOrderIds.size > 0) {
|
|
2097
|
-
const now = Date.now();
|
|
2098
|
-
let prunedCount = 0;
|
|
2099
|
-
for (const [orderId, markedAt] of this._staleCleanedOrderIds) {
|
|
2100
|
-
if (now - markedAt > this._staleCleanupRetentionMs) {
|
|
2101
|
-
this._staleCleanedOrderIds.delete(orderId);
|
|
2102
|
-
prunedCount++;
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
if (prunedCount > 0) {
|
|
2106
|
-
this.manager.logger.log(`[STALE-CLEANUP] Pruned ${prunedCount} expired stale-cleaned order IDs ` +
|
|
2107
|
-
`(retention=${this._staleCleanupRetentionMs}ms, remaining=${this._staleCleanedOrderIds.size})`, 'debug');
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
} // End while(_incomingFillQueue)
|
|
2111
|
-
this._markGridActivity('fill processing end');
|
|
2112
|
-
// Reset the fill-consumer watchdog on the success path. The
|
|
2113
|
-
// counter is only incremented by _scheduleFillConsumerRestart's
|
|
2114
|
-
// catch handler; without this reset, a fill pattern of
|
|
2115
|
-
// F-S-F-S-F-S-F-S-F would still reach the max and the consumer
|
|
2116
|
-
// would stop being re-scheduled.
|
|
2117
|
-
this._consecutiveConsumeFailures = 0;
|
|
2118
|
-
this._consumeFailureFirstAt = 0;
|
|
2119
|
-
});
|
|
2120
|
-
}
|
|
2121
|
-
catch (err) {
|
|
2122
|
-
const isCredentialOutage = this._isCredentialDaemonError(err);
|
|
2123
|
-
if (pendingFillKeysForCurrentCycle.size > 0) {
|
|
2124
|
-
const flushReason = isCredentialOutage
|
|
2125
|
-
? 'credential-outage-verified-fills'
|
|
2126
|
-
: 'fill-cycle-error-verified-fills';
|
|
2127
|
-
if (isCredentialOutage) {
|
|
2128
|
-
this._credentialRecoveryNeeded = true;
|
|
2129
|
-
this._suspendGridPersistenceForCredentialOutage(`credential outage during fill processing: ${err.message}`);
|
|
2130
|
-
}
|
|
2131
|
-
try {
|
|
2132
|
-
await this._flushProcessedFillPersistenceForKeys(pendingFillKeysForCurrentCycle, flushReason, { throwOnError: true });
|
|
2133
|
-
const credentialSuffix = isCredentialOutage
|
|
2134
|
-
? '; grid persistence is suspended until recovery'
|
|
2135
|
-
: '';
|
|
2136
|
-
this.manager?.logger?.log?.(`[FILL-DEDUP] Persisted ${pendingFillKeysForCurrentCycle.size} verified processed-fill write(s) after fill cycle error${credentialSuffix}.`, isCredentialOutage ? 'warn' : 'info');
|
|
2137
|
-
}
|
|
2138
|
-
catch (flushErr) {
|
|
2139
|
-
this.manager?.logger?.log?.(`[FILL-DEDUP] Failed to persist verified fill keys during fill error handling: ${flushErr.message}`, 'warn');
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
if (isCredentialOutage && pendingFillKeysForCurrentCycle.size === 0) {
|
|
2143
|
-
this._credentialRecoveryNeeded = true;
|
|
2144
|
-
this._suspendGridPersistenceForCredentialOutage(`credential outage during fill processing: ${err.message}`);
|
|
2145
|
-
}
|
|
2146
|
-
this._log(`Error processing fills: ${err.message}`, 'error');
|
|
2147
|
-
if (err.stack)
|
|
2148
|
-
this._log(err.stack, 'error');
|
|
2149
|
-
}
|
|
2150
|
-
// Post-processing: If new fills arrived while processing, schedule another cycle
|
|
2151
|
-
// SAFE: Done outside lock context, no async work in finally block
|
|
2152
|
-
if (!this._shuttingDown && this._incomingFillQueue.length > 0) {
|
|
2153
|
-
this._scheduleFillConsumerRestart(chainOrders);
|
|
2154
|
-
}
|
|
681
|
+
return DexbotFillRuntime.consumeFillQueue(this, chainOrders);
|
|
2155
682
|
}
|
|
2156
683
|
/**
|
|
2157
684
|
* Process fills during bootstrap phase using the standard fill pipeline.
|
|
@@ -2171,98 +698,7 @@ class DEXBot {
|
|
|
2171
698
|
* @returns {Promise<void>}
|
|
2172
699
|
*/
|
|
2173
700
|
async _processFillsWithBootstrapMode(chainOrders) {
|
|
2174
|
-
|
|
2175
|
-
return;
|
|
2176
|
-
const startTime = Date.now();
|
|
2177
|
-
const fills = this._incomingFillQueue.splice(0);
|
|
2178
|
-
const validFills = [];
|
|
2179
|
-
const processedFillKeys = new Set();
|
|
2180
|
-
let requiresOpenOrdersSync = false;
|
|
2181
|
-
// 1. Validate and deduplicate fills
|
|
2182
|
-
for (const fill of fills) {
|
|
2183
|
-
if (!fill || fill.op?.[0] !== 4)
|
|
2184
|
-
continue;
|
|
2185
|
-
const fillOp = fill.op[1];
|
|
2186
|
-
const gridOrder = this.manager.orders.get(fillOp.order_id) ||
|
|
2187
|
-
Array.from(this.manager.orders.values()).find((o) => o.orderId === fillOp.order_id);
|
|
2188
|
-
if (!gridOrder) {
|
|
2189
|
-
// CRITICAL FIX: Even if order not in grid, we must still credit the fill proceeds
|
|
2190
|
-
// This can happen when fills arrive after an order was marked VIRTUAL during sequential processing
|
|
2191
|
-
let orphanFillKey = buildFillKey(fill);
|
|
2192
|
-
if (!orphanFillKey) {
|
|
2193
|
-
orphanFillKey = this._buildOrphanFillFallbackKey(fill);
|
|
2194
|
-
}
|
|
2195
|
-
if (orphanFillKey && !this._isNewFillKey(orphanFillKey, processedFillKeys, '[BOOTSTRAP]', fillOp.order_id)) {
|
|
2196
|
-
continue;
|
|
2197
|
-
}
|
|
2198
|
-
this.manager.logger.log(`[BOOTSTRAP] Processing funds for unknown order ${fillOp.order_id} (not in grid but crediting proceeds)`, 'warn');
|
|
2199
|
-
const accountingResult = await this._applyReplaySafeOrphanFillAccounting(fill, fillOp, {
|
|
2200
|
-
context: 'BOOTSTRAP'
|
|
2201
|
-
});
|
|
2202
|
-
if (accountingResult.status === 'missing_key') {
|
|
2203
|
-
requiresOpenOrdersSync = true;
|
|
2204
|
-
}
|
|
2205
|
-
continue;
|
|
2206
|
-
}
|
|
2207
|
-
const trackedFillKey = buildFillKey(fill);
|
|
2208
|
-
if (trackedFillKey && !this._isNewFillKey(trackedFillKey, processedFillKeys, '[BOOTSTRAP]', fillOp.order_id)) {
|
|
2209
|
-
continue;
|
|
2210
|
-
}
|
|
2211
|
-
this.manager.lockOrders([gridOrder.id]);
|
|
2212
|
-
try {
|
|
2213
|
-
const accountingResult = await this._applyReplaySafeTrackedFillAccounting(fill, fillOp, {
|
|
2214
|
-
context: 'BOOTSTRAP',
|
|
2215
|
-
replayMessage: (op) => `[BOOTSTRAP] Replay detected for ${op.order_id}; skipping duplicate bootstrap rebalance`
|
|
2216
|
-
});
|
|
2217
|
-
if (accountingResult.status === 'missing_key') {
|
|
2218
|
-
requiresOpenOrdersSync = true;
|
|
2219
|
-
continue;
|
|
2220
|
-
}
|
|
2221
|
-
if (accountingResult.status !== 'applied') {
|
|
2222
|
-
continue;
|
|
2223
|
-
}
|
|
2224
|
-
validFills.push({ ...fill, gridOrder });
|
|
2225
|
-
const fillType = gridOrder.type === ORDER_TYPES.BUY ? 'BUY' : 'SELL';
|
|
2226
|
-
this._log(`[BOOTSTRAP] Fill detected: ${fillType} order (${fillOp.is_maker !== false ? 'maker' : 'taker'})`);
|
|
2227
|
-
}
|
|
2228
|
-
finally {
|
|
2229
|
-
this.manager.unlockOrders([gridOrder.id]);
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
if (requiresOpenOrdersSync) {
|
|
2233
|
-
this._log('[BOOTSTRAP] Falling back to open-orders sync for fill(s) missing replay-safe history identifiers', 'warn');
|
|
2234
|
-
const bootstrapChainOpenOrders = await chainOrders.readOpenOrders(this.accountId);
|
|
2235
|
-
const syncResult = await this.manager.syncFromOpenOrders(bootstrapChainOpenOrders);
|
|
2236
|
-
if (syncResult.filledOrders?.length > 0) {
|
|
2237
|
-
const queuedOrderIds = new Set(validFills.map(fill => fill?.gridOrder?.orderId).filter(Boolean));
|
|
2238
|
-
for (const filledOrder of syncResult.filledOrders) {
|
|
2239
|
-
if (!filledOrder?.orderId || queuedOrderIds.has(filledOrder.orderId))
|
|
2240
|
-
continue;
|
|
2241
|
-
validFills.push({ gridOrder: filledOrder });
|
|
2242
|
-
queuedOrderIds.add(filledOrder.orderId);
|
|
2243
|
-
}
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
await this._flushProcessedFillPersistence('bootstrap-batch');
|
|
2247
|
-
if (validFills.length === 0)
|
|
2248
|
-
return;
|
|
2249
|
-
// 2. Process fills through the standard fill pipeline.
|
|
2250
|
-
// This handles same-side replacement, symmetric rebalance, and dust cleanup
|
|
2251
|
-
// via the same path used by the post-reset flow.
|
|
2252
|
-
try {
|
|
2253
|
-
this._log(`[BOOTSTRAP] Processing ${validFills.length} fill(s) through standard pipeline`, 'info');
|
|
2254
|
-
const filledOrders = validFills.map(f => f.gridOrder);
|
|
2255
|
-
const result = await this._processFillsWithBatching(filledOrders, new Set(), '[BOOTSTRAP] fill processing');
|
|
2256
|
-
if (result.aborted) {
|
|
2257
|
-
this._warn('[BOOTSTRAP] Aborted batch due to illegal state; skipping grid persistence this cycle');
|
|
2258
|
-
}
|
|
2259
|
-
this._metrics.fillsProcessed += validFills.length;
|
|
2260
|
-
this._metrics.fillProcessingTimeMs += Date.now() - startTime;
|
|
2261
|
-
}
|
|
2262
|
-
catch (err) {
|
|
2263
|
-
this._warn(`[BOOTSTRAP] Error processing fills: ${err.message}`);
|
|
2264
|
-
this.manager.logger.log(`[BOOTSTRAP] Fill error: ${err.message}`, 'error');
|
|
2265
|
-
}
|
|
701
|
+
return DexbotFillRuntime.processFillsWithBootstrapMode(this, chainOrders);
|
|
2266
702
|
}
|
|
2267
703
|
/**
|
|
2268
704
|
* Set up account identifier and configure global context.
|
|
@@ -2288,7 +724,7 @@ class DEXBot {
|
|
|
2288
724
|
* @throws {Error} If initialization fails or preferredAccount is missing.
|
|
2289
725
|
*/
|
|
2290
726
|
async initialize(vaultSecret = null) {
|
|
2291
|
-
await waitForConnected(TIMING.CONNECTION_TIMEOUT_MS);
|
|
727
|
+
await waitForConnected(constants_1.TIMING.CONNECTION_TIMEOUT_MS);
|
|
2292
728
|
if (this.config && this.config.preferredAccount) {
|
|
2293
729
|
try {
|
|
2294
730
|
let privateKey = null;
|
|
@@ -2298,7 +734,7 @@ class DEXBot {
|
|
|
2298
734
|
catch (err) {
|
|
2299
735
|
if (vaultSecret)
|
|
2300
736
|
throw err;
|
|
2301
|
-
this._warn(`Credential daemon probe failed: ${err
|
|
737
|
+
this._warn(`Credential daemon probe failed: ${(0, errors_1.getErrorMessage)(err)}. Falling back to interactive authentication.`);
|
|
2302
738
|
}
|
|
2303
739
|
if (!privateKey) {
|
|
2304
740
|
const unlockSecret = await chainKeys.authenticate();
|
|
@@ -2311,7 +747,7 @@ class DEXBot {
|
|
|
2311
747
|
if (chainKeys.isMasterPasswordFailure(err)) {
|
|
2312
748
|
throw err;
|
|
2313
749
|
}
|
|
2314
|
-
this._warn(`Auto-selection of preferredAccount failed: ${err
|
|
750
|
+
this._warn(`Auto-selection of preferredAccount failed: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
2315
751
|
// dexbot.ts has fallback to selectAccount, bot.ts throws
|
|
2316
752
|
if (typeof chainOrders.selectAccount === 'function') {
|
|
2317
753
|
const accountData = await chainOrders.selectAccount();
|
|
@@ -2332,43 +768,7 @@ class DEXBot {
|
|
|
2332
768
|
* @returns {Promise<void>}
|
|
2333
769
|
*/
|
|
2334
770
|
async placeInitialOrders() {
|
|
2335
|
-
|
|
2336
|
-
const mgrLogFile = this.config?.name ? path.join(PATHS.LOGS_DIR, `${this.config.name}.log`) : undefined;
|
|
2337
|
-
this.manager = new OrderManager({ ...this.config, logFile: mgrLogFile });
|
|
2338
|
-
this.manager.accountOrders = this.accountOrders;
|
|
2339
|
-
}
|
|
2340
|
-
this._wireStructuralGridResyncRequest();
|
|
2341
|
-
this.manager.startBootstrap();
|
|
2342
|
-
try {
|
|
2343
|
-
try {
|
|
2344
|
-
const botFunds = this.config && this.config.botFunds ? this.config.botFunds : {};
|
|
2345
|
-
const needsPercent = (v) => typeof v === 'string' && v.includes('%');
|
|
2346
|
-
if ((needsPercent(botFunds.buy) || needsPercent(botFunds.sell)) && (this.accountId || this.account)) {
|
|
2347
|
-
if (typeof this.manager._fetchAccountBalancesAndSetTotals === 'function') {
|
|
2348
|
-
await this.manager._fetchAccountBalancesAndSetTotals();
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
catch (errFetch) {
|
|
2353
|
-
this._warn(`Could not fetch account totals before initializing grid: ${errFetch && errFetch.message ? errFetch.message : errFetch}`);
|
|
2354
|
-
}
|
|
2355
|
-
await Grid.initializeGrid(this.manager);
|
|
2356
|
-
if (this.config.dryRun) {
|
|
2357
|
-
this.manager.logger.log('Dry run enabled, skipping on-chain order placement.', 'info');
|
|
2358
|
-
await this.manager.persistGrid();
|
|
2359
|
-
return;
|
|
2360
|
-
}
|
|
2361
|
-
this.manager.logger.log('Placing initial orders on-chain...', 'info');
|
|
2362
|
-
const ordersToActivate = this.manager.getInitialOrdersToActivate();
|
|
2363
|
-
const orderGroups = this._buildOutsideInPairGroupsForOrders(ordersToActivate);
|
|
2364
|
-
for (const group of orderGroups) {
|
|
2365
|
-
await this.updateOrdersOnChainPlan({ ordersToPlace: group });
|
|
2366
|
-
}
|
|
2367
|
-
await this.manager.persistGrid();
|
|
2368
|
-
}
|
|
2369
|
-
finally {
|
|
2370
|
-
this.manager.finishBootstrap();
|
|
2371
|
-
}
|
|
771
|
+
return DexbotStartupRuntime.placeInitialOrdersImpl(this);
|
|
2372
772
|
}
|
|
2373
773
|
/**
|
|
2374
774
|
* Build outside-in pair groups for initial order placement.
|
|
@@ -2376,11 +776,7 @@ class DEXBot {
|
|
|
2376
776
|
* @returns {Array<Array<Object>>} Grouped order arrays
|
|
2377
777
|
*/
|
|
2378
778
|
_buildOutsideInPairGroupsForOrders(orders) {
|
|
2379
|
-
return
|
|
2380
|
-
isValid: Boolean,
|
|
2381
|
-
getType: o => o.type,
|
|
2382
|
-
getPrice: o => o.price,
|
|
2383
|
-
});
|
|
779
|
+
return dexbot_cow_runtime_1.default.buildOutsideInPairGroupsForOrders(orders);
|
|
2384
780
|
}
|
|
2385
781
|
/**
|
|
2386
782
|
* Build outside-in pair groups for create entry contexts.
|
|
@@ -2388,18 +784,14 @@ class DEXBot {
|
|
|
2388
784
|
* @returns {Array<Array<Object>>} Grouped entry arrays
|
|
2389
785
|
*/
|
|
2390
786
|
_buildOutsideInPairGroupsForCreateEntries(createEntries) {
|
|
2391
|
-
return
|
|
2392
|
-
isValid: e => Boolean(e?.context?.order),
|
|
2393
|
-
getType: e => e.context.order.type,
|
|
2394
|
-
getPrice: e => e.context.order.price,
|
|
2395
|
-
});
|
|
787
|
+
return dexbot_cow_runtime_1.default.buildOutsideInPairGroupsForCreateEntries(createEntries);
|
|
2396
788
|
}
|
|
2397
789
|
/**
|
|
2398
790
|
* Resolve the centralized fill batch cap.
|
|
2399
791
|
* @returns {number} Positive maximum number of fill-driven rotations per broadcast cycle
|
|
2400
792
|
*/
|
|
2401
793
|
_getMaxFillBatchSize() {
|
|
2402
|
-
return Math.max(1, this.config.fillProcessing?.MAX_FILL_BATCH_SIZE ?? FILL_PROCESSING.MAX_FILL_BATCH_SIZE);
|
|
794
|
+
return Math.max(1, this.config.fillProcessing?.MAX_FILL_BATCH_SIZE ?? constants_1.FILL_PROCESSING.MAX_FILL_BATCH_SIZE);
|
|
2403
795
|
}
|
|
2404
796
|
/**
|
|
2405
797
|
* Extract operation results from a batch transaction result.
|
|
@@ -2408,19 +800,7 @@ class DEXBot {
|
|
|
2408
800
|
* @returns {Array} Array of operation result entries
|
|
2409
801
|
*/
|
|
2410
802
|
_extractOperationResults(result, warnContext = '') {
|
|
2411
|
-
|
|
2412
|
-
if (Array.isArray(extracted))
|
|
2413
|
-
return extracted;
|
|
2414
|
-
if (result) {
|
|
2415
|
-
const resultType = Array.isArray(result) ? 'array' : typeof result;
|
|
2416
|
-
const keySummary = (resultType === 'object' && !Array.isArray(result))
|
|
2417
|
-
? Object.keys(result).slice(0, 8).join(',')
|
|
2418
|
-
: '';
|
|
2419
|
-
const contextSuffix = warnContext ? ` (${warnContext})` : '';
|
|
2420
|
-
const keysSuffix = keySummary ? `; keys=[${keySummary}]` : '';
|
|
2421
|
-
this.manager?.logger?.log(`[COW] Unrecognized operation_results shape${contextSuffix}; defaulting to empty results. resultType=${resultType}${keysSuffix}`, 'warn');
|
|
2422
|
-
}
|
|
2423
|
-
return [];
|
|
803
|
+
return dexbot_cow_runtime_1.default.extractOperationResults(result, warnContext, this.manager?.logger?.log?.bind(this.manager?.logger));
|
|
2424
804
|
}
|
|
2425
805
|
/**
|
|
2426
806
|
* Find CREATE operation contexts whose broadcast result did not include a chain order id.
|
|
@@ -2430,19 +810,7 @@ class DEXBot {
|
|
|
2430
810
|
* @returns {Array<{index:number, ctx:Object}>} Missing create result contexts.
|
|
2431
811
|
*/
|
|
2432
812
|
_findMissingCreateResultContexts(operationResults, opContexts) {
|
|
2433
|
-
|
|
2434
|
-
if (!Array.isArray(opContexts))
|
|
2435
|
-
return missing;
|
|
2436
|
-
for (let i = 0; i < opContexts.length; i++) {
|
|
2437
|
-
const ctx = opContexts[i];
|
|
2438
|
-
if (ctx?.kind !== 'create')
|
|
2439
|
-
continue;
|
|
2440
|
-
const chainOrderId = operationResults?.[i]?.[1];
|
|
2441
|
-
if (!chainOrderId || !/^1\.7\.\d+$/.test(String(chainOrderId))) {
|
|
2442
|
-
missing.push({ index: i, ctx });
|
|
2443
|
-
}
|
|
2444
|
-
}
|
|
2445
|
-
return missing;
|
|
813
|
+
return dexbot_cow_runtime_1.default.findMissingCreateResultContexts(operationResults, opContexts);
|
|
2446
814
|
}
|
|
2447
815
|
/**
|
|
2448
816
|
* Run an immediate chain sync after a successful CREATE broadcast returned incomplete ids.
|
|
@@ -2456,42 +824,7 @@ class DEXBot {
|
|
|
2456
824
|
* @returns {Promise<void>}
|
|
2457
825
|
*/
|
|
2458
826
|
async _recoverAfterMissingCreateResults(reason = 'missing create operation results') {
|
|
2459
|
-
|
|
2460
|
-
const accountRef = this.accountId || this.account?.id || this.account;
|
|
2461
|
-
if (!accountRef || !this.manager || !chainOrders?.readOpenOrders) {
|
|
2462
|
-
this.manager?.logger?.log?.(`[COW] Recovery sync unavailable after ${reason}`, 'warn');
|
|
2463
|
-
return;
|
|
2464
|
-
}
|
|
2465
|
-
const preRecoveryMissingCreateBlockers = Array.isArray(this.manager._lastUnmatchedChainOrders)
|
|
2466
|
-
? this.manager._lastUnmatchedChainOrders
|
|
2467
|
-
.filter(order => order?.reason === 'missing-create-result')
|
|
2468
|
-
.map(order => ({ ...order }))
|
|
2469
|
-
: [];
|
|
2470
|
-
const openOrders = await chainOrders.readOpenOrders(accountRef);
|
|
2471
|
-
const recoveryResult = await this.manager.syncFromOpenOrders(openOrders, {
|
|
2472
|
-
skipAccounting: false,
|
|
2473
|
-
});
|
|
2474
|
-
this._preserveMissingCreateBlockersAfterRecovery(preRecoveryMissingCreateBlockers, recoveryResult);
|
|
2475
|
-
// Persist any master grid mutations from the recovery sync. The
|
|
2476
|
-
// caller returned from the COW catch handler before reaching the
|
|
2477
|
-
// success-path persistGrid.
|
|
2478
|
-
if (typeof this.manager.persistGrid === 'function') {
|
|
2479
|
-
await this.manager.persistGrid();
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
catch (err) {
|
|
2483
|
-
this.manager?.logger?.log?.(`[COW] CRITICAL: Recovery sync failed after ${reason}: ${err.message}`, 'error');
|
|
2484
|
-
if (typeof this.manager?.requestStructuralGridResync === 'function') {
|
|
2485
|
-
try {
|
|
2486
|
-
await this.manager.requestStructuralGridResync(`recovery sync failed after ${reason}`, {
|
|
2487
|
-
error: err.message
|
|
2488
|
-
});
|
|
2489
|
-
}
|
|
2490
|
-
catch (scheduleErr) {
|
|
2491
|
-
this.manager?.logger?.log?.(`[COW] CRITICAL: Failed to schedule structural resync after recovery failure: ${scheduleErr.message}`, 'error');
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
}
|
|
827
|
+
return dexbot_cow_runtime_1.default.recoverAfterMissingCreateResults(this, reason);
|
|
2495
828
|
}
|
|
2496
829
|
/**
|
|
2497
830
|
* Restore unresolved missing-create blockers after recovery if sync did not adopt them.
|
|
@@ -2501,30 +834,7 @@ class DEXBot {
|
|
|
2501
834
|
* @returns {void}
|
|
2502
835
|
*/
|
|
2503
836
|
_preserveMissingCreateBlockersAfterRecovery(blockers, recoveryResult) {
|
|
2504
|
-
|
|
2505
|
-
return;
|
|
2506
|
-
const adoptedSlotIds = new Set((Array.isArray(recoveryResult?.updatedOrders) ? recoveryResult.updatedOrders : [])
|
|
2507
|
-
.filter(order => order?.id && order?.orderId)
|
|
2508
|
-
.map(order => order.id));
|
|
2509
|
-
const unresolvedBlockers = blockers.filter(blocker => !blocker.slotId || !adoptedSlotIds.has(blocker.slotId));
|
|
2510
|
-
if (unresolvedBlockers.length === 0)
|
|
2511
|
-
return;
|
|
2512
|
-
const currentUnmatched = Array.isArray(this.manager._lastUnmatchedChainOrders)
|
|
2513
|
-
? this.manager._lastUnmatchedChainOrders
|
|
2514
|
-
: [];
|
|
2515
|
-
const currentKeys = new Set(currentUnmatched.map(order => `${order.reason || ''}:${order.slotId || ''}:${order.operationIndex ?? ''}`));
|
|
2516
|
-
const restored = [...currentUnmatched];
|
|
2517
|
-
for (const blocker of unresolvedBlockers) {
|
|
2518
|
-
const key = `${blocker.reason || ''}:${blocker.slotId || ''}:${blocker.operationIndex ?? ''}`;
|
|
2519
|
-
if (!currentKeys.has(key))
|
|
2520
|
-
restored.push({ ...blocker });
|
|
2521
|
-
}
|
|
2522
|
-
if (restored.length !== currentUnmatched.length) {
|
|
2523
|
-
this.manager._lastUnmatchedChainOrders = restored;
|
|
2524
|
-
this.manager._lastUnmatchedChainOrdersAt = Date.now();
|
|
2525
|
-
this.manager.logger?.log?.(`[COW] Preserving ${restored.length - currentUnmatched.length} missing-create blocker(s) after recovery sync; ` +
|
|
2526
|
-
`chain snapshot did not account for the affected slot(s).`, 'warn');
|
|
2527
|
-
}
|
|
837
|
+
return dexbot_cow_runtime_1.default.preserveMissingCreateBlockersAfterRecovery(this, blockers, recoveryResult);
|
|
2528
838
|
}
|
|
2529
839
|
/**
|
|
2530
840
|
* Merge missing CREATE result contexts into manager._lastUnmatchedChainOrders.
|
|
@@ -2538,42 +848,7 @@ class DEXBot {
|
|
|
2538
848
|
* @returns {void}
|
|
2539
849
|
*/
|
|
2540
850
|
_markMissingCreateResultsAsStructuralBlocker(missingCreateResults) {
|
|
2541
|
-
|
|
2542
|
-
? missingCreateResults.map(item => {
|
|
2543
|
-
const order = item.ctx?.order || {};
|
|
2544
|
-
const fingerprint = [
|
|
2545
|
-
`type=${order.type || 'unknown'}`,
|
|
2546
|
-
`price=${Format.formatPrice6(order.price)}`,
|
|
2547
|
-
`size=${Format.formatAmount(order.size)}`
|
|
2548
|
-
].join(',');
|
|
2549
|
-
return {
|
|
2550
|
-
chainOrderId: 'unknown',
|
|
2551
|
-
type: order.type || null,
|
|
2552
|
-
price: order.price,
|
|
2553
|
-
size: order.size,
|
|
2554
|
-
slotId: order.id || item.ctx?.id || null,
|
|
2555
|
-
reason: 'missing-create-result',
|
|
2556
|
-
operationIndex: item.index,
|
|
2557
|
-
fingerprint,
|
|
2558
|
-
};
|
|
2559
|
-
})
|
|
2560
|
-
: [];
|
|
2561
|
-
if (this.manager && blockers.length > 0) {
|
|
2562
|
-
const existing = Array.isArray(this.manager._lastUnmatchedChainOrders)
|
|
2563
|
-
? this.manager._lastUnmatchedChainOrders
|
|
2564
|
-
: [];
|
|
2565
|
-
const keys = new Set(existing.map(order => `${order.reason || ''}:${order.slotId || ''}:${order.operationIndex ?? ''}`));
|
|
2566
|
-
const merged = [...existing];
|
|
2567
|
-
for (const blocker of blockers) {
|
|
2568
|
-
const key = `${blocker.reason || ''}:${blocker.slotId || ''}:${blocker.operationIndex ?? ''}`;
|
|
2569
|
-
if (!keys.has(key)) {
|
|
2570
|
-
merged.push(blocker);
|
|
2571
|
-
keys.add(key);
|
|
2572
|
-
}
|
|
2573
|
-
}
|
|
2574
|
-
this.manager._lastUnmatchedChainOrders = merged;
|
|
2575
|
-
this.manager._lastUnmatchedChainOrdersAt = Date.now();
|
|
2576
|
-
}
|
|
851
|
+
return dexbot_cow_runtime_1.default.markMissingCreateResultsAsStructuralBlocker(this, missingCreateResults);
|
|
2577
852
|
}
|
|
2578
853
|
/**
|
|
2579
854
|
* Format an unmatched chain order/blocker for COW logs.
|
|
@@ -2582,7 +857,7 @@ class DEXBot {
|
|
|
2582
857
|
* @returns {string} Compact human-readable diagnostic.
|
|
2583
858
|
*/
|
|
2584
859
|
_formatUnmatchedChainOrderForLog(order) {
|
|
2585
|
-
return
|
|
860
|
+
return dexbot_cow_runtime_1.default.formatUnmatchedChainOrderForLog(order);
|
|
2586
861
|
}
|
|
2587
862
|
/**
|
|
2588
863
|
* Record a pending CREATE broadcast on the manager.
|
|
@@ -2605,35 +880,7 @@ class DEXBot {
|
|
|
2605
880
|
* @returns {void}
|
|
2606
881
|
*/
|
|
2607
882
|
_recordPendingBroadcast(entry) {
|
|
2608
|
-
|
|
2609
|
-
return;
|
|
2610
|
-
if (!this.manager._pendingBroadcasts || !(this.manager._pendingBroadcasts instanceof Map)) {
|
|
2611
|
-
this.manager._pendingBroadcasts = new Map();
|
|
2612
|
-
}
|
|
2613
|
-
const fingerprint = buildCreateOpFingerprint({
|
|
2614
|
-
side: entry.order.type,
|
|
2615
|
-
assetA: this.manager?.assets?.assetA?.id,
|
|
2616
|
-
assetB: this.manager?.assets?.assetB?.id,
|
|
2617
|
-
sellInt: entry.finalInts?.sell,
|
|
2618
|
-
receiveInt: entry.finalInts?.receive,
|
|
2619
|
-
slotId: entry.order.id
|
|
2620
|
-
});
|
|
2621
|
-
if (!fingerprint) {
|
|
2622
|
-
this.manager.logger.log?.(`[COW] Skipped pending-broadcast record: could not build fingerprint for ${entry.order?.id || 'unknown'}`, 'warn');
|
|
2623
|
-
return;
|
|
2624
|
-
}
|
|
2625
|
-
this.manager._pendingBroadcasts.set(fingerprint, {
|
|
2626
|
-
fingerprint,
|
|
2627
|
-
opIndex: entry.opIndex,
|
|
2628
|
-
ctxIndex: entry.ctxIndex,
|
|
2629
|
-
slotId: entry.order.id,
|
|
2630
|
-
orderId: entry.order.id,
|
|
2631
|
-
orderType: entry.order.type,
|
|
2632
|
-
order: entry.order,
|
|
2633
|
-
finalInts: entry.finalInts,
|
|
2634
|
-
batchId: this._currentBatchId || null,
|
|
2635
|
-
recordedAt: Date.now()
|
|
2636
|
-
});
|
|
883
|
+
return dexbot_cow_runtime_1.default.recordPendingBroadcast(this, entry);
|
|
2637
884
|
}
|
|
2638
885
|
/**
|
|
2639
886
|
* Clear the pending-broadcast cache.
|
|
@@ -2644,9 +891,7 @@ class DEXBot {
|
|
|
2644
891
|
* _reconcileAfterUncertainBroadcast before calling this).
|
|
2645
892
|
*/
|
|
2646
893
|
_clearPendingBroadcasts() {
|
|
2647
|
-
|
|
2648
|
-
this.manager._pendingBroadcasts.clear();
|
|
2649
|
-
}
|
|
894
|
+
return dexbot_cow_runtime_1.default.clearPendingBroadcasts(this.manager?._pendingBroadcasts);
|
|
2650
895
|
}
|
|
2651
896
|
/**
|
|
2652
897
|
* Build a fingerprint for an on-chain order so it can be matched against
|
|
@@ -2657,19 +902,7 @@ class DEXBot {
|
|
|
2657
902
|
* @returns {string|null} Fingerprint or null on bad input
|
|
2658
903
|
*/
|
|
2659
904
|
_buildChainOrderFingerprint(chainOrder, slotId) {
|
|
2660
|
-
|
|
2661
|
-
return null;
|
|
2662
|
-
const normalized = this._normalizeChainOrderForPendingMatch(chainOrder);
|
|
2663
|
-
if (!normalized)
|
|
2664
|
-
return null;
|
|
2665
|
-
return buildCreateOpFingerprint({
|
|
2666
|
-
side: normalized.side,
|
|
2667
|
-
assetA: normalized.assetA,
|
|
2668
|
-
assetB: normalized.assetB,
|
|
2669
|
-
sellInt: normalized.sellInt,
|
|
2670
|
-
receiveInt: normalized.receiveInt,
|
|
2671
|
-
slotId
|
|
2672
|
-
});
|
|
905
|
+
return dexbot_cow_runtime_1.default.buildChainOrderFingerprint(this, chainOrder, slotId);
|
|
2673
906
|
}
|
|
2674
907
|
/**
|
|
2675
908
|
* Normalize raw BitShares limit_order_object data into the integer tuple
|
|
@@ -2683,41 +916,7 @@ class DEXBot {
|
|
|
2683
916
|
* @returns {{side: string, assetA: string, assetB: string, sellInt: number, receiveInt: number}|null}
|
|
2684
917
|
*/
|
|
2685
918
|
_normalizeChainOrderForPendingMatch(chainOrder) {
|
|
2686
|
-
|
|
2687
|
-
return null;
|
|
2688
|
-
const assetA = this.manager?.assets?.assetA?.id;
|
|
2689
|
-
const assetB = this.manager?.assets?.assetB?.id;
|
|
2690
|
-
if (!assetA || !assetB)
|
|
2691
|
-
return null;
|
|
2692
|
-
const explicitSide = (chainOrder.type === 'buy' || chainOrder.type === 'sell')
|
|
2693
|
-
? chainOrder.type
|
|
2694
|
-
: null;
|
|
2695
|
-
const explicitSell = chainOrder.sellInt ?? chainOrder.sell;
|
|
2696
|
-
const explicitReceive = chainOrder.receiveInt ?? chainOrder.receive;
|
|
2697
|
-
if (explicitSide && Number.isFinite(Number(explicitSell)) && Number.isFinite(Number(explicitReceive))) {
|
|
2698
|
-
return {
|
|
2699
|
-
side: explicitSide,
|
|
2700
|
-
assetA,
|
|
2701
|
-
assetB,
|
|
2702
|
-
sellInt: Number(explicitSell),
|
|
2703
|
-
receiveInt: Number(explicitReceive)
|
|
2704
|
-
};
|
|
2705
|
-
}
|
|
2706
|
-
const base = chainOrder.sell_price?.base;
|
|
2707
|
-
const quote = chainOrder.sell_price?.quote;
|
|
2708
|
-
if (!base || !quote || !base.asset_id || !quote.asset_id)
|
|
2709
|
-
return null;
|
|
2710
|
-
const baseAmount = Number(base.amount);
|
|
2711
|
-
const quoteAmount = Number(quote.amount);
|
|
2712
|
-
if (!Number.isFinite(baseAmount) || !Number.isFinite(quoteAmount))
|
|
2713
|
-
return null;
|
|
2714
|
-
if (base.asset_id === assetA && quote.asset_id === assetB) {
|
|
2715
|
-
return { side: 'sell', assetA, assetB, sellInt: baseAmount, receiveInt: quoteAmount };
|
|
2716
|
-
}
|
|
2717
|
-
if (base.asset_id === assetB && quote.asset_id === assetA) {
|
|
2718
|
-
return { side: 'buy', assetA, assetB, sellInt: baseAmount, receiveInt: quoteAmount };
|
|
2719
|
-
}
|
|
2720
|
-
return null;
|
|
919
|
+
return dexbot_cow_runtime_1.default.normalizeChainOrderForPendingMatch(this, chainOrder);
|
|
2721
920
|
}
|
|
2722
921
|
/**
|
|
2723
922
|
* Find a chain order that matches a planned slot using price+size proximity.
|
|
@@ -2735,56 +934,7 @@ class DEXBot {
|
|
|
2735
934
|
* @returns {Object|null} Matching chain order, or null
|
|
2736
935
|
*/
|
|
2737
936
|
_findChainOrderForSlot(chainOrders, slotId, planned) {
|
|
2738
|
-
|
|
2739
|
-
return null;
|
|
2740
|
-
const assetA = this.manager?.assets?.assetA?.id;
|
|
2741
|
-
const assetB = this.manager?.assets?.assetB?.id;
|
|
2742
|
-
if (!assetA || !assetB)
|
|
2743
|
-
return null;
|
|
2744
|
-
// 1. Exact fingerprint match.
|
|
2745
|
-
for (const o of chainOrders) {
|
|
2746
|
-
const fp = this._buildChainOrderFingerprint(o, slotId);
|
|
2747
|
-
if (fp && this.manager._pendingBroadcasts?.has(fp)) {
|
|
2748
|
-
return o;
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
if (!planned || !Number.isFinite(Number(planned.sell)) || !Number.isFinite(Number(planned.receive))) {
|
|
2752
|
-
return null;
|
|
2753
|
-
}
|
|
2754
|
-
// 2. Near match: same side, sell int within 1, receive int within 1% or 2 units.
|
|
2755
|
-
const targetSell = Number(planned.sell);
|
|
2756
|
-
const targetReceive = Number(planned.receive);
|
|
2757
|
-
const plannedSide = planned.orderType ||
|
|
2758
|
-
planned.side ||
|
|
2759
|
-
this.manager._pendingBroadcasts?.get?.(planned.fingerprint)?.orderType ||
|
|
2760
|
-
this.manager.orders.get(slotId)?.type;
|
|
2761
|
-
if (plannedSide !== 'buy' && plannedSide !== 'sell') {
|
|
2762
|
-
return null;
|
|
2763
|
-
}
|
|
2764
|
-
let best = null;
|
|
2765
|
-
let bestDistance = Infinity;
|
|
2766
|
-
for (const o of chainOrders) {
|
|
2767
|
-
const normalized = this._normalizeChainOrderForPendingMatch(o);
|
|
2768
|
-
if (!normalized)
|
|
2769
|
-
continue;
|
|
2770
|
-
if (normalized.side !== plannedSide)
|
|
2771
|
-
continue;
|
|
2772
|
-
const sell = Number(normalized.sellInt);
|
|
2773
|
-
const receive = Number(normalized.receiveInt);
|
|
2774
|
-
if (!Number.isFinite(sell) || !Number.isFinite(receive))
|
|
2775
|
-
continue;
|
|
2776
|
-
const sellDelta = Math.abs(sell - targetSell);
|
|
2777
|
-
const receiveDelta = Math.abs(receive - targetReceive);
|
|
2778
|
-
const receiveTol = Math.max(2, Math.floor(targetReceive * 0.01));
|
|
2779
|
-
if (sellDelta > 1 || receiveDelta > receiveTol)
|
|
2780
|
-
continue;
|
|
2781
|
-
const distance = sellDelta * 1000 + receiveDelta;
|
|
2782
|
-
if (distance < bestDistance) {
|
|
2783
|
-
best = o;
|
|
2784
|
-
bestDistance = distance;
|
|
2785
|
-
}
|
|
2786
|
-
}
|
|
2787
|
-
return best;
|
|
937
|
+
return dexbot_cow_runtime_1.default.findChainOrderForSlot(this, chainOrders, slotId, planned);
|
|
2788
938
|
}
|
|
2789
939
|
/**
|
|
2790
940
|
* Reconcile a broadcast whose chain state is unknown.
|
|
@@ -2800,286 +950,23 @@ class DEXBot {
|
|
|
2800
950
|
* for a matching chain order. If found: adopt it (set the opContext's
|
|
2801
951
|
* chainOrderId and continue with the existing planned slot).
|
|
2802
952
|
* 3. For pending entries with no chain match: virtualize (mark the
|
|
2803
|
-
* opContext as discarded; the planned slot stays empty until the
|
|
2804
|
-
* next planning cycle).
|
|
2805
|
-
* 4. Persist + log a structured [COW][UNCERTAIN] summary.
|
|
2806
|
-
*
|
|
2807
|
-
* After this method returns, the bot has either adopted the on-chain
|
|
2808
|
-
* result (good case — chain accepted but we didn't see the reply) or
|
|
2809
|
-
* accepted the discard (chain rejected or never received the op).
|
|
2810
|
-
*
|
|
2811
|
-
* @param {BroadcastUncertainError} err - The thrown error
|
|
2812
|
-
* @param {Array<Object>} opContexts - Original opContexts from the failed batch
|
|
2813
|
-
* @returns {Promise<Object>} Result object compatible with batch return shape
|
|
2814
|
-
*/
|
|
2815
|
-
async _reconcileAfterUncertainBroadcast(err, opContexts, options = {}) {
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
}
|
|
2821
|
-
return this._reconcileAfterUncertainBroadcastImpl(err, opContexts, options);
|
|
2822
|
-
}
|
|
2823
|
-
async _reconcileAfterUncertainBroadcastImpl(err, opContexts, options) {
|
|
2824
|
-
const startedAt = Date.now();
|
|
2825
|
-
const pending = (this.manager && this.manager._pendingBroadcasts instanceof Map)
|
|
2826
|
-
? Array.from(this.manager._pendingBroadcasts.values())
|
|
2827
|
-
: [];
|
|
2828
|
-
const createContextCount = opContexts.filter(c => c && c.kind === 'create').length;
|
|
2829
|
-
const nonCreateContextCount = opContexts.length - createContextCount;
|
|
2830
|
-
this.manager.logger.log(`[COW][UNCERTAIN] batchId=${err?.batchId || 'n/a'} ops=${opContexts.length} ` +
|
|
2831
|
-
`creates=${createContextCount} nonCreates=${nonCreateContextCount} ` +
|
|
2832
|
-
`staleSinceMs=${err?.timeoutMs || 'n/a'}. Entering reconcile-then-decide.`, 'warn');
|
|
2833
|
-
if (!chainOrders?.readOpenOrders) {
|
|
2834
|
-
this.manager.logger.log('[COW][UNCERTAIN] readOpenOrders unavailable; falling back to structural resync only.', 'error');
|
|
2835
|
-
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
2836
|
-
await this.manager.requestStructuralGridResync('broadcast uncertain — readOpenOrders unavailable', { batchId: err?.batchId || null });
|
|
2837
|
-
}
|
|
2838
|
-
this._clearPendingBroadcasts();
|
|
2839
|
-
return { executed: false, hadRotation: false, uncertain: true };
|
|
2840
|
-
}
|
|
2841
|
-
// 1. Read the chain
|
|
2842
|
-
const accountRef = this.accountId || this.account?.id || this.account;
|
|
2843
|
-
let chainSnapshot = [];
|
|
2844
|
-
try {
|
|
2845
|
-
chainSnapshot = await chainOrders.readOpenOrders(accountRef);
|
|
2846
|
-
}
|
|
2847
|
-
catch (readErr) {
|
|
2848
|
-
this.manager.logger.log(`[COW][UNCERTAIN] readOpenOrders failed: ${readErr?.message || readErr}. ` +
|
|
2849
|
-
`Falling back to structural resync.`, 'error');
|
|
2850
|
-
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
2851
|
-
await this.manager.requestStructuralGridResync('broadcast uncertain — readOpenOrders failed', { batchId: err?.batchId || null, error: readErr?.message || String(readErr) });
|
|
2852
|
-
}
|
|
2853
|
-
this._clearPendingBroadcasts();
|
|
2854
|
-
return { executed: false, hadRotation: false, uncertain: true };
|
|
2855
|
-
}
|
|
2856
|
-
const adopted = [];
|
|
2857
|
-
let discarded = [];
|
|
2858
|
-
// 2. For each pending broadcast, look for a chain match.
|
|
2859
|
-
for (const entry of pending) {
|
|
2860
|
-
const match = this._findChainOrderForSlot(chainSnapshot, entry.slotId, {
|
|
2861
|
-
sell: entry.finalInts?.sell,
|
|
2862
|
-
receive: entry.finalInts?.receive,
|
|
2863
|
-
orderType: entry.orderType || entry.order?.type,
|
|
2864
|
-
fingerprint: entry.fingerprint
|
|
2865
|
-
});
|
|
2866
|
-
if (match) {
|
|
2867
|
-
adopted.push({
|
|
2868
|
-
slotId: entry.slotId,
|
|
2869
|
-
chainOrderId: match.id,
|
|
2870
|
-
orderType: entry.orderType || entry.order?.type,
|
|
2871
|
-
});
|
|
2872
|
-
this.manager._pendingBroadcasts.delete(entry.fingerprint);
|
|
2873
|
-
}
|
|
2874
|
-
else {
|
|
2875
|
-
// Preserve full entry context so the recheck below can use
|
|
2876
|
-
// both the fingerprint match path (against _pendingBroadcasts)
|
|
2877
|
-
// and the near-match path (via finalInts/orderType), not just
|
|
2878
|
-
// slotId alone.
|
|
2879
|
-
discarded.push(entry);
|
|
2880
|
-
}
|
|
2881
|
-
}
|
|
2882
|
-
// ---- RACE MITIGATION: block-delay recheck before discarding creates ----
|
|
2883
|
-
// BROADCAST_DEADLINE fires while the transaction may still be valid
|
|
2884
|
-
// and land in the next block. Wait for up to 3 blocks (~1 BitShares
|
|
2885
|
-
// block interval each) and re-check before permanently discarding.
|
|
2886
|
-
const UNCERTAIN_RECHECK_MAX_ATTEMPTS = this.config.pipelineTiming?.RETRY_MAX_ATTEMPTS ?? PIPELINE_TIMING.RETRY_MAX_ATTEMPTS; // 3
|
|
2887
|
-
const UNCERTAIN_RECHECK_INTERVAL_MS = TIMING.MILLISECONDS_PER_SECOND * 3; // 3s
|
|
2888
|
-
let recheckRound = 0;
|
|
2889
|
-
while (discarded.length > 0 && recheckRound < UNCERTAIN_RECHECK_MAX_ATTEMPTS) {
|
|
2890
|
-
recheckRound++;
|
|
2891
|
-
await new Promise(r => setTimeout(r, UNCERTAIN_RECHECK_INTERVAL_MS));
|
|
2892
|
-
let freshSnapshot;
|
|
2893
|
-
try {
|
|
2894
|
-
freshSnapshot = await chainOrders.readOpenOrders(accountRef);
|
|
2895
|
-
}
|
|
2896
|
-
catch {
|
|
2897
|
-
break;
|
|
2898
|
-
}
|
|
2899
|
-
const stillDiscarded = [];
|
|
2900
|
-
for (const entry of discarded) {
|
|
2901
|
-
const match = this._findChainOrderForSlot(freshSnapshot, entry.slotId, {
|
|
2902
|
-
sell: entry.finalInts?.sell,
|
|
2903
|
-
receive: entry.finalInts?.receive,
|
|
2904
|
-
orderType: entry.orderType || entry.order?.type,
|
|
2905
|
-
fingerprint: entry.fingerprint
|
|
2906
|
-
});
|
|
2907
|
-
if (match) {
|
|
2908
|
-
adopted.push({
|
|
2909
|
-
slotId: entry.slotId,
|
|
2910
|
-
chainOrderId: match.id,
|
|
2911
|
-
orderType: entry.orderType || entry.order?.type,
|
|
2912
|
-
});
|
|
2913
|
-
this.manager.logger.log(`[COW][UNCERTAIN] CREATE re-adopted after ${recheckRound} block(s): ` +
|
|
2914
|
-
`${entry.slotId}->${match.id}`, 'info');
|
|
2915
|
-
}
|
|
2916
|
-
else {
|
|
2917
|
-
stillDiscarded.push(entry);
|
|
2918
|
-
}
|
|
2919
|
-
}
|
|
2920
|
-
discarded = stillDiscarded;
|
|
2921
|
-
}
|
|
2922
|
-
// 3. Re-read chain with latest state. The initial snapshot may be
|
|
2923
|
-
// stale for non-CREATE ops (updates/cancels) not tracked in
|
|
2924
|
-
// _pendingBroadcasts — the seed cause of the slot-115 cascade.
|
|
2925
|
-
let latestSnapshot;
|
|
2926
|
-
try {
|
|
2927
|
-
latestSnapshot = await chainOrders.readOpenOrders(accountRef);
|
|
2928
|
-
}
|
|
2929
|
-
catch {
|
|
2930
|
-
latestSnapshot = chainSnapshot;
|
|
2931
|
-
}
|
|
2932
|
-
// Always sync to link adopted chain orders into grid slots.
|
|
2933
|
-
// Otherwise an adopted slot stays VIRTUAL and the next COW cycle
|
|
2934
|
-
// places a duplicate order at the same price.
|
|
2935
|
-
let hadRotation = false;
|
|
2936
|
-
if (latestSnapshot && latestSnapshot.length > 0 && this.manager?.syncFromOpenOrders) {
|
|
2937
|
-
try {
|
|
2938
|
-
await this.manager.syncFromOpenOrders(latestSnapshot, {
|
|
2939
|
-
skipAccounting: true,
|
|
2940
|
-
});
|
|
2941
|
-
hadRotation = true;
|
|
2942
|
-
}
|
|
2943
|
-
catch (syncErr) {
|
|
2944
|
-
this.manager.logger.log(`[COW][UNCERTAIN] syncFromOpenOrders failed during recovery: ${syncErr?.message || syncErr}`, 'error');
|
|
2945
|
-
// Fallback: reload from persisted snapshot + re-sync.
|
|
2946
|
-
this.manager.logger.log('[COW][UNCERTAIN] syncFromOpenOrders failed; falling back to full grid reload from persisted snapshot', 'warn');
|
|
2947
|
-
const fallbackResult = await this._recoverFromPersistedGrid();
|
|
2948
|
-
if (fallbackResult.success) {
|
|
2949
|
-
hadRotation = true;
|
|
2950
|
-
}
|
|
2951
|
-
else {
|
|
2952
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Persisted grid reload failed: ${fallbackResult.reason}. ` +
|
|
2953
|
-
`Escalating to structural resync.`, 'warn');
|
|
2954
|
-
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
2955
|
-
this.manager.requestStructuralGridResync('cow-uncertain-recovery-failed', { reason: fallbackResult.reason }).catch((escalateErr) => {
|
|
2956
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Escalation to structural resync failed: ${escalateErr.message}`, 'error');
|
|
2957
|
-
});
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
}
|
|
2961
|
-
}
|
|
2962
|
-
// 4. Log structured summary.
|
|
2963
|
-
const elapsedMs = Date.now() - startedAt;
|
|
2964
|
-
const heartbeatAgeMs = this._lastBroadcastHeartbeatAt
|
|
2965
|
-
? Date.now() - this._lastBroadcastHeartbeatAt
|
|
2966
|
-
: null;
|
|
2967
|
-
this.manager.logger.log(`[COW][UNCERTAIN] batchId=${err?.batchId || 'n/a'} ops=${opContexts.length} ` +
|
|
2968
|
-
`staleSinceMs=${err?.timeoutMs || 'n/a'} heartbeatAgeMs=${heartbeatAgeMs ?? 'n/a'} ` +
|
|
2969
|
-
`adopted=${adopted.length} discarded=${discarded.length} recheckRounds=${recheckRound} elapsedMs=${elapsedMs}`, adopted.length > 0 ? 'info' : 'warn');
|
|
2970
|
-
if (adopted.length > 0) {
|
|
2971
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Adopted chain orders: ${adopted
|
|
2972
|
-
.map(a => `${a.slotId}->${a.chainOrderId}`)
|
|
2973
|
-
.join(', ')}`, 'info');
|
|
2974
|
-
}
|
|
2975
|
-
if (discarded.length > 0) {
|
|
2976
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Discarded planned CREATEs (no chain match after ${recheckRound} recheck(s)): ${discarded
|
|
2977
|
-
.map(d => d.slotId)
|
|
2978
|
-
.join(', ')}`, 'warn');
|
|
2979
|
-
// Restore target grid sizes for discarded CREATEs so the slots are
|
|
2980
|
-
// not left as virtual/0 after recovery. Without this, the slot stays
|
|
2981
|
-
// at size 0 (set by the fill handler) and is never reactivated until
|
|
2982
|
-
// a fresh fill cycle triggers another rebalance that happens to succeed.
|
|
2983
|
-
// entry.order is the pending-broadcast target order (captured at broadcast
|
|
2984
|
-
// time); its .id matches entry.slotId — the lookup below keys on slotId.
|
|
2985
|
-
for (const entry of discarded) {
|
|
2986
|
-
if (entry.order && entry.slotId) {
|
|
2987
|
-
const current = this.manager.orders.get(entry.slotId);
|
|
2988
|
-
if (current && current.state === ORDER_STATES.VIRTUAL && !current.orderId) {
|
|
2989
|
-
try {
|
|
2990
|
-
await this.manager._applyOrderUpdate({ ...entry.order, state: ORDER_STATES.VIRTUAL, orderId: null }, 'uncertain-recovery-restore-size', { skipAccounting: true, fee: 0 });
|
|
2991
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Restored target size for discarded CREATE slot ${entry.slotId} (size: ${entry.order.size})`, 'info');
|
|
2992
|
-
}
|
|
2993
|
-
catch (restoreErr) {
|
|
2994
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Failed to restore size for slot ${entry.slotId}: ${restoreErr?.message || restoreErr}`, 'warn');
|
|
2995
|
-
}
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
}
|
|
3000
|
-
// ---- Structural resync safeguard after skipAccounting restore ----
|
|
3001
|
-
// The discarded CREATE restore above used skipAccounting: true to avoid
|
|
3002
|
-
// double-counting when the structural resync recalculates accounting
|
|
3003
|
-
// from scratch. Ensure one is scheduled — if already in-flight (timer
|
|
3004
|
-
// or running), the existing resync will handle the accounting fix.
|
|
3005
|
-
if (discarded.length > 0 && typeof this.manager?.requestStructuralGridResync === 'function') {
|
|
3006
|
-
const alreadyScheduled = this._structuralGridResyncRunning || this._structuralGridResyncTimer;
|
|
3007
|
-
if (!alreadyScheduled) {
|
|
3008
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Scheduling structural resync after discarded CREATE restore ` +
|
|
3009
|
-
`(skipAccounting used — resync needed for fund recalculation)`, 'info');
|
|
3010
|
-
this.manager.requestStructuralGridResync('cow-uncertain-accounting-repair', { discardedCount: discarded.length }).catch((err) => {
|
|
3011
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Failed to schedule accounting repair resync: ${err.message}`, 'error');
|
|
3012
|
-
});
|
|
3013
|
-
}
|
|
3014
|
-
else {
|
|
3015
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Structural resync already ${this._structuralGridResyncRunning ? 'running' : 'scheduled'}; ` +
|
|
3016
|
-
`it will repair accounting after discarded CREATE restore`, 'debug');
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
this._clearPendingBroadcasts();
|
|
3020
|
-
// Post-recovery safety net: if there are still unmatched chain
|
|
3021
|
-
// orders after reconcile-then-decide, cancel ONE per cycle. The cap
|
|
3022
|
-
// is enforced inside the helper via _autoCancelOrphanCycleMarker.
|
|
3023
|
-
try {
|
|
3024
|
-
const autoCancelResult = await this._autoCancelOneUnmatchedOrphan();
|
|
3025
|
-
if (autoCancelResult.cancelled) {
|
|
3026
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Auto-cancelled orphan ${autoCancelResult.orderId} ` +
|
|
3027
|
-
`after recovery (cap=1/cycle).`, 'info');
|
|
3028
|
-
}
|
|
3029
|
-
}
|
|
3030
|
-
catch (orphanErr) {
|
|
3031
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Auto-cancel pass failed: ${orphanErr?.message || orphanErr}`, 'warn');
|
|
3032
|
-
}
|
|
3033
|
-
// Boundary shift recovery: the COW batch that should have committed the
|
|
3034
|
-
// boundary shift from this fill cycle failed before commit. Only COUNT
|
|
3035
|
-
// ADOPTED CREATEs — orders that actually landed on-chain. Discarded
|
|
3036
|
-
// CREATEs represent orders that never existed, so the grid must NOT
|
|
3037
|
-
// shift the boundary for them. The next fill cycle's
|
|
3038
|
-
// calculateTargetGrid → deriveTargetBoundary will recompute the correct
|
|
3039
|
-
// boundary from scratch for any discarded slots.
|
|
3040
|
-
//
|
|
3041
|
-
// NOTE: Direct mutation of manager.boundaryIdx outside a COW commit
|
|
3042
|
-
// violates the invariant stated in grid.ts:1217-1220 (boundary must only
|
|
3043
|
-
// be updated atomically inside _commitWorkingGrid). This is intentional
|
|
3044
|
-
// here because the COW commit already failed — the invariant was already
|
|
3045
|
-
// broken by the broadcast uncertainty, and the adjustment only restores
|
|
3046
|
-
// the state to what the successful commit would have produced for
|
|
3047
|
-
// orders that are confirmed on-chain.
|
|
3048
|
-
if (hadRotation && this.manager && adopted.length > 0) {
|
|
3049
|
-
let boundaryShift = 0;
|
|
3050
|
-
for (const entry of adopted) {
|
|
3051
|
-
if (entry.orderType === ORDER_TYPES.SELL) {
|
|
3052
|
-
boundaryShift--; // SELL CREATE → fill was BUY → boundary LEFT
|
|
3053
|
-
}
|
|
3054
|
-
else if (entry.orderType === ORDER_TYPES.BUY) {
|
|
3055
|
-
boundaryShift++; // BUY CREATE → fill was SELL → boundary RIGHT
|
|
3056
|
-
}
|
|
3057
|
-
}
|
|
3058
|
-
if (boundaryShift !== 0) {
|
|
3059
|
-
const oldIdx = this.manager.boundaryIdx;
|
|
3060
|
-
if (typeof oldIdx === 'number') {
|
|
3061
|
-
const maxIdx = Math.max(0, (this.manager.orders?.size || 1) - 1);
|
|
3062
|
-
const newIdx = Math.max(0, Math.min(maxIdx, oldIdx + boundaryShift));
|
|
3063
|
-
if (newIdx !== oldIdx) {
|
|
3064
|
-
this.manager.boundaryIdx = newIdx;
|
|
3065
|
-
this.manager.logger.log(`[COW][UNCERTAIN] Boundary adjusted by ${boundaryShift} ` +
|
|
3066
|
-
`(${adopted.length} adopted CREATE(s), ${discarded.length} discarded): ` +
|
|
3067
|
-
`${oldIdx} → ${newIdx}. Next COW cycle will generate replacement opposite-side orders.`, 'warn');
|
|
3068
|
-
if (typeof this.manager._markGridDirty === 'function') {
|
|
3069
|
-
this.manager._markGridDirty();
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
}
|
|
3073
|
-
}
|
|
3074
|
-
}
|
|
3075
|
-
// Persist master grid mutations from the reconciliation sync and any
|
|
3076
|
-
// orphan auto-cancellation that ran above. These apply outside the COW
|
|
3077
|
-
// broadcast path and would otherwise be in-memory only. The boundary
|
|
3078
|
-
// adjustment above is also persisted here.
|
|
3079
|
-
if (typeof this.manager?.persistGrid === 'function') {
|
|
3080
|
-
await this.manager.persistGrid();
|
|
3081
|
-
}
|
|
3082
|
-
return { executed: false, hadRotation, uncertain: true, adopted, discarded };
|
|
953
|
+
* opContext as discarded; the planned slot stays empty until the
|
|
954
|
+
* next planning cycle).
|
|
955
|
+
* 4. Persist + log a structured [COW][UNCERTAIN] summary.
|
|
956
|
+
*
|
|
957
|
+
* After this method returns, the bot has either adopted the on-chain
|
|
958
|
+
* result (good case — chain accepted but we didn't see the reply) or
|
|
959
|
+
* accepted the discard (chain rejected or never received the op).
|
|
960
|
+
*
|
|
961
|
+
* @param {BroadcastUncertainError} err - The thrown error
|
|
962
|
+
* @param {Array<Object>} opContexts - Original opContexts from the failed batch
|
|
963
|
+
* @returns {Promise<Object>} Result object compatible with batch return shape
|
|
964
|
+
*/
|
|
965
|
+
async _reconcileAfterUncertainBroadcast(err, opContexts, options = {}) {
|
|
966
|
+
return dexbot_cow_runtime_1.default.reconcileAfterUncertainBroadcast(this, err, opContexts, options);
|
|
967
|
+
}
|
|
968
|
+
async _reconcileAfterUncertainBroadcastImpl(err, opContexts, options) {
|
|
969
|
+
return dexbot_cow_runtime_1.default.reconcileAfterUncertainBroadcastImpl(this, err, opContexts, options);
|
|
3083
970
|
}
|
|
3084
971
|
/**
|
|
3085
972
|
* Auto-cancel a price-drift orphan from the unmatched-order snapshot.
|
|
@@ -3107,72 +994,7 @@ class DEXBot {
|
|
|
3107
994
|
* @returns {Promise<{cancelled: boolean, orderId?: string, reason?: string}>}
|
|
3108
995
|
*/
|
|
3109
996
|
async _autoCancelOneUnmatchedOrphan() {
|
|
3110
|
-
|
|
3111
|
-
// FIX 4: Higher cap during recovery mode so orphan backlog clears faster.
|
|
3112
|
-
const recoveryActive = this.manager?._recoveryState?.structuralResyncRequested === true;
|
|
3113
|
-
const cycleCap = recoveryActive ? 5 : 1;
|
|
3114
|
-
if (this._autoCancelOrphanCycleMarker === cycleId) {
|
|
3115
|
-
if (this._autoCancelOrphanSubCount >= cycleCap) {
|
|
3116
|
-
return { cancelled: false, reason: 'cap-reached-this-cycle', subCount: this._autoCancelOrphanSubCount };
|
|
3117
|
-
}
|
|
3118
|
-
}
|
|
3119
|
-
else {
|
|
3120
|
-
// Reset sub-count on first call this cycle
|
|
3121
|
-
this._autoCancelOrphanCycleMarker = cycleId;
|
|
3122
|
-
this._autoCancelOrphanSubCount = 0;
|
|
3123
|
-
}
|
|
3124
|
-
const pending = (this.manager && this.manager._pendingBroadcasts instanceof Map)
|
|
3125
|
-
? this.manager._pendingBroadcasts.size
|
|
3126
|
-
: 0;
|
|
3127
|
-
if (pending > 0) {
|
|
3128
|
-
return { cancelled: false, reason: 'pending-broadcasts-active' };
|
|
3129
|
-
}
|
|
3130
|
-
const unmatched = Array.isArray(this.manager?._lastUnmatchedChainOrders)
|
|
3131
|
-
? this.manager._lastUnmatchedChainOrders
|
|
3132
|
-
: [];
|
|
3133
|
-
if (unmatched.length === 0) {
|
|
3134
|
-
return { cancelled: false, reason: 'no-unmatched' };
|
|
3135
|
-
}
|
|
3136
|
-
// Check for fingerprinted entries first — these came from a pending
|
|
3137
|
-
// broadcast (missing-create-result) and must be handled by the recovery
|
|
3138
|
-
// path, not by auto-cancel. The first fingerprinted entry is checked
|
|
3139
|
-
// regardless of its reason field.
|
|
3140
|
-
const fingerprinted = unmatched.find(u => u && u.fingerprint);
|
|
3141
|
-
if (fingerprinted) {
|
|
3142
|
-
return { cancelled: false, reason: 'fingerprinted-handle-via-recovery' };
|
|
3143
|
-
}
|
|
3144
|
-
// Only cancel price-drift orphans — these are surplus orders that
|
|
3145
|
-
// drifted away from their assigned slot price and have no adoptable
|
|
3146
|
-
// grid slot. All other unmatched orders (duplicate-price-level,
|
|
3147
|
-
// already-matched-slot, etc.) are adoptable positions that the structural
|
|
3148
|
-
// resync will integrate into the grid — cancelling them destroys capital.
|
|
3149
|
-
const target = unmatched.find(u => u && u.reason === 'price-drift-orphan');
|
|
3150
|
-
if (!target) {
|
|
3151
|
-
return { cancelled: false, reason: 'no-price-drift-orphan', message: 'no price-drift orphan to cancel; other unmatched orders are adoptable' };
|
|
3152
|
-
}
|
|
3153
|
-
const orderId = target.id || target.orderId || target.chainOrderId;
|
|
3154
|
-
if (!orderId) {
|
|
3155
|
-
return { cancelled: false, reason: 'no-orderId' };
|
|
3156
|
-
}
|
|
3157
|
-
if (!chainOrders?.cancelOrder) {
|
|
3158
|
-
return { cancelled: false, reason: 'cancelOrder-unavailable' };
|
|
3159
|
-
}
|
|
3160
|
-
try {
|
|
3161
|
-
await chainOrders.cancelOrder(this.account, this.privateKey, orderId);
|
|
3162
|
-
if (typeof chainOrders.recordOwnCancel === 'function') {
|
|
3163
|
-
chainOrders.recordOwnCancel(orderId);
|
|
3164
|
-
}
|
|
3165
|
-
// Increment sub-count only after a confirmed successful cancel.
|
|
3166
|
-
// Failed cancels do not consume the per-cycle cap budget.
|
|
3167
|
-
this._autoCancelOrphanSubCount++;
|
|
3168
|
-
this.manager.logger.log(`[COW] Auto-cancelled ${this._autoCancelOrphanSubCount}/${unmatched.length} unmatched chain order ` +
|
|
3169
|
-
`(${this._formatUnmatchedChainOrderForLog(target)}) — per-cycle cap=${cycleCap}.`, 'warn');
|
|
3170
|
-
return { cancelled: true, orderId };
|
|
3171
|
-
}
|
|
3172
|
-
catch (err) {
|
|
3173
|
-
this.manager.logger.log(`[COW] Auto-cancel of unmatched chain order ${orderId} failed: ${err?.message || err}`, 'error');
|
|
3174
|
-
return { cancelled: false, reason: 'cancel-failed', error: err?.message || String(err) };
|
|
3175
|
-
}
|
|
997
|
+
return dexbot_cow_runtime_1.default.autoCancelOneUnmatchedOrphan(this);
|
|
3176
998
|
}
|
|
3177
999
|
// Pair mode applies only when create contexts include both BUY and SELL.
|
|
3178
1000
|
// Single-side create batches intentionally remain a single executeBatch.
|
|
@@ -3182,21 +1004,7 @@ class DEXBot {
|
|
|
3182
1004
|
* @returns {boolean} True if pair mode should be used
|
|
3183
1005
|
*/
|
|
3184
1006
|
_shouldExecuteCreatePairMode(opContexts) {
|
|
3185
|
-
|
|
3186
|
-
return false;
|
|
3187
|
-
if (!opContexts.every(ctx => ctx?.kind === 'create' && ctx?.order))
|
|
3188
|
-
return false;
|
|
3189
|
-
let hasBuy = false;
|
|
3190
|
-
let hasSell = false;
|
|
3191
|
-
for (const ctx of opContexts) {
|
|
3192
|
-
if (ctx.order.type === ORDER_TYPES.BUY)
|
|
3193
|
-
hasBuy = true;
|
|
3194
|
-
if (ctx.order.type === ORDER_TYPES.SELL)
|
|
3195
|
-
hasSell = true;
|
|
3196
|
-
if (hasBuy && hasSell)
|
|
3197
|
-
return true;
|
|
3198
|
-
}
|
|
3199
|
-
return false;
|
|
1007
|
+
return dexbot_cow_runtime_1.default.shouldExecuteCreatePairMode(this, opContexts);
|
|
3200
1008
|
}
|
|
3201
1009
|
/**
|
|
3202
1010
|
* Execute operations with retry on BroadcastUncertainError.
|
|
@@ -3208,36 +1016,7 @@ class DEXBot {
|
|
|
3208
1016
|
* the full operations array would duplicate those creates on chain.
|
|
3209
1017
|
*/
|
|
3210
1018
|
async _executeWithRetryOnUncertain(operations, opContexts) {
|
|
3211
|
-
|
|
3212
|
-
for (let attempt = 1;; attempt++) {
|
|
3213
|
-
try {
|
|
3214
|
-
return await this._executeOperationsWithStrategy(operations, opContexts);
|
|
3215
|
-
}
|
|
3216
|
-
catch (err) {
|
|
3217
|
-
const isRetriable = err instanceof BroadcastUncertainError
|
|
3218
|
-
&& !err.partialOnChainState
|
|
3219
|
-
&& attempt <= MAX_RETRIES;
|
|
3220
|
-
if (isRetriable) {
|
|
3221
|
-
this.manager.logger.log(`[COW] Broadcast uncertain (attempt ${attempt}/${MAX_RETRIES + 1}), retrying...`, 'warn');
|
|
3222
|
-
await this._ensureCredentialDaemonWritable('COW batch retry');
|
|
3223
|
-
// Reconcile before retry — stale ops would fail "not found".
|
|
3224
|
-
try {
|
|
3225
|
-
const accountRef = this.accountId || this.account?.id || this.account;
|
|
3226
|
-
const freshChain = await chainOrders.readOpenOrders(accountRef);
|
|
3227
|
-
if (freshChain.length > 0 && this.manager?.syncFromOpenOrders) {
|
|
3228
|
-
await this.manager.syncFromOpenOrders(freshChain, {
|
|
3229
|
-
skipAccounting: true,
|
|
3230
|
-
});
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3233
|
-
catch (syncErr) {
|
|
3234
|
-
this.manager.logger.log(`[COW] Pre-retry sync failed (non-fatal): ${syncErr?.message || syncErr}`, 'warn');
|
|
3235
|
-
}
|
|
3236
|
-
continue;
|
|
3237
|
-
}
|
|
3238
|
-
throw err;
|
|
3239
|
-
}
|
|
3240
|
-
}
|
|
1019
|
+
return dexbot_cow_runtime_1.default.executeWithRetryOnUncertain(this, operations, opContexts);
|
|
3241
1020
|
}
|
|
3242
1021
|
/**
|
|
3243
1022
|
* Execute blockchain operations with appropriate strategy (single batch or pair mode).
|
|
@@ -3246,63 +1025,7 @@ class DEXBot {
|
|
|
3246
1025
|
* @returns {Promise<{result: Object, opContexts: Array}>} Execution result with contexts
|
|
3247
1026
|
*/
|
|
3248
1027
|
async _executeOperationsWithStrategy(operations, opContexts) {
|
|
3249
|
-
|
|
3250
|
-
const result = await chainOrders.executeBatch(this.account, this.privateKey, operations);
|
|
3251
|
-
return { result, opContexts };
|
|
3252
|
-
}
|
|
3253
|
-
const createEntries = [];
|
|
3254
|
-
for (let i = 0; i < operations.length; i++) {
|
|
3255
|
-
createEntries.push({
|
|
3256
|
-
operation: operations[i],
|
|
3257
|
-
context: opContexts[i],
|
|
3258
|
-
});
|
|
3259
|
-
}
|
|
3260
|
-
const groups = this._buildOutsideInPairGroupsForCreateEntries(createEntries);
|
|
3261
|
-
const mergedOperationResults = [];
|
|
3262
|
-
const mergedRawResults = [];
|
|
3263
|
-
const mergedContexts = [];
|
|
3264
|
-
// Grouped execution is fail-fast, but NOT atomic across groups.
|
|
3265
|
-
// Each group is a separate on-chain transaction; if a later group fails,
|
|
3266
|
-
// earlier groups may already be confirmed on-chain.
|
|
3267
|
-
for (let idx = 0; idx < groups.length; idx++) {
|
|
3268
|
-
const group = groups[idx];
|
|
3269
|
-
const groupOps = group.map(e => e.operation);
|
|
3270
|
-
const groupContexts = group.map(e => e.context);
|
|
3271
|
-
this.manager.logger.log(`[COW] Broadcasting create pair group ${idx + 1}/${groups.length} (${groupOps.length} op${groupOps.length > 1 ? 's' : ''}, outside->center)`, 'info');
|
|
3272
|
-
let groupResult;
|
|
3273
|
-
try {
|
|
3274
|
-
groupResult = await chainOrders.executeBatch(this.account, this.privateKey, groupOps);
|
|
3275
|
-
}
|
|
3276
|
-
catch (err) {
|
|
3277
|
-
const groupsBroadcast = idx;
|
|
3278
|
-
const groupsTotal = groups.length;
|
|
3279
|
-
const broadcastedOperationCount = mergedContexts.length;
|
|
3280
|
-
this.manager.logger.log(`[COW] Grouped create execution failed at group ${idx + 1}/${groupsTotal}; ${groupsBroadcast} group(s) already broadcast (${broadcastedOperationCount} op context(s)). Partial on-chain state is possible.`, 'error');
|
|
3281
|
-
err.partialOnChainState = groupsBroadcast > 0;
|
|
3282
|
-
err.groupsBroadcast = groupsBroadcast;
|
|
3283
|
-
err.groupsTotal = groupsTotal;
|
|
3284
|
-
err.broadcastedOperationCount = broadcastedOperationCount;
|
|
3285
|
-
throw err;
|
|
3286
|
-
}
|
|
3287
|
-
const groupOpResults = this._extractOperationResults(groupResult);
|
|
3288
|
-
mergedOperationResults.push(...groupOpResults);
|
|
3289
|
-
mergedRawResults.push(groupResult?.raw || null);
|
|
3290
|
-
mergedContexts.push(...groupContexts);
|
|
3291
|
-
}
|
|
3292
|
-
return {
|
|
3293
|
-
result: {
|
|
3294
|
-
success: true,
|
|
3295
|
-
raw: {
|
|
3296
|
-
grouped: true,
|
|
3297
|
-
groupsExecuted: groups.length,
|
|
3298
|
-
groupResults: mergedRawResults,
|
|
3299
|
-
},
|
|
3300
|
-
operation_results: mergedOperationResults,
|
|
3301
|
-
grouped: true,
|
|
3302
|
-
groupsExecuted: groups.length
|
|
3303
|
-
},
|
|
3304
|
-
opContexts: mergedContexts
|
|
3305
|
-
};
|
|
1028
|
+
return dexbot_cow_runtime_1.default.executeOperationsWithStrategy(this, operations, opContexts);
|
|
3306
1029
|
}
|
|
3307
1030
|
/**
|
|
3308
1031
|
* Validate that operations can be executed with available funds before broadcasting.
|
|
@@ -3314,94 +1037,7 @@ class DEXBot {
|
|
|
3314
1037
|
* @private
|
|
3315
1038
|
*/
|
|
3316
1039
|
_validateOperationFunds(operations, assetA, assetB) {
|
|
3317
|
-
|
|
3318
|
-
return { isValid: true, summary: 'No operations to validate' };
|
|
3319
|
-
}
|
|
3320
|
-
const { blockchainToFloat, floatToBlockchainInt, quantizeFloat } = require('./order/utils/math');
|
|
3321
|
-
const snap = this.manager.getChainFundsSnapshot();
|
|
3322
|
-
const netRequiredFunds = { [assetA.id]: 0, [assetB.id]: 0 };
|
|
3323
|
-
const runningRequiredFunds = { [assetA.id]: 0, [assetB.id]: 0 };
|
|
3324
|
-
const peakRequiredFunds = { [assetA.id]: 0, [assetB.id]: 0 };
|
|
3325
|
-
// Sum amounts and check individual order sizes
|
|
3326
|
-
for (const op of operations) {
|
|
3327
|
-
if (!op?.op_data)
|
|
3328
|
-
continue;
|
|
3329
|
-
let sellAssetId = null;
|
|
3330
|
-
let sellAmountInt = 0;
|
|
3331
|
-
if (op.op_name === 'limit_order_create') {
|
|
3332
|
-
sellAssetId = op.op_data.amount_to_sell?.asset_id;
|
|
3333
|
-
sellAmountInt = op.op_data.amount_to_sell?.amount;
|
|
3334
|
-
}
|
|
3335
|
-
else if (op.op_name === 'limit_order_update') {
|
|
3336
|
-
// In limit_order_update, new_price.base is the amount to sell
|
|
3337
|
-
sellAssetId = op.op_data.new_price?.base?.asset_id;
|
|
3338
|
-
sellAmountInt = op.op_data.new_price?.base?.amount;
|
|
3339
|
-
}
|
|
3340
|
-
if (sellAssetId && (sellAmountInt !== undefined && sellAmountInt !== null)) {
|
|
3341
|
-
const precision = (sellAssetId === assetA.id) ? assetA.precision : assetB.precision;
|
|
3342
|
-
const assetSymbol = (sellAssetId === assetA.id) ? assetA.symbol : assetB.symbol;
|
|
3343
|
-
// CRITICAL SAFETY CHECK: Ensure amount is greater than zero
|
|
3344
|
-
if (Number(sellAmountInt) <= 0) {
|
|
3345
|
-
return {
|
|
3346
|
-
isValid: false,
|
|
3347
|
-
summary: `[VALIDATION] CRITICAL: Zero amount order detected for ${assetSymbol} (assetId=${sellAssetId})`,
|
|
3348
|
-
violations: [{ asset: assetSymbol, sizeInt: sellAmountInt, reason: 'Zero amount' }]
|
|
3349
|
-
};
|
|
3350
|
-
}
|
|
3351
|
-
// Track signed per-op deltas in operation order.
|
|
3352
|
-
// CREATE consumes full amount; UPDATE consumes/releases delta.
|
|
3353
|
-
let signedDelta = 0;
|
|
3354
|
-
if (op.op_name === 'limit_order_update') {
|
|
3355
|
-
const deltaAssetId = op.op_data.delta_amount_to_sell?.asset_id;
|
|
3356
|
-
const deltaSellInt = op.op_data.delta_amount_to_sell?.amount;
|
|
3357
|
-
if (deltaAssetId === sellAssetId && Number.isFinite(Number(deltaSellInt))) {
|
|
3358
|
-
signedDelta = blockchainToFloat(deltaSellInt, precision);
|
|
3359
|
-
}
|
|
3360
|
-
}
|
|
3361
|
-
else {
|
|
3362
|
-
signedDelta = blockchainToFloat(sellAmountInt, precision);
|
|
3363
|
-
}
|
|
3364
|
-
netRequiredFunds[sellAssetId] = quantizeFloat((netRequiredFunds[sellAssetId] || 0) + signedDelta, precision);
|
|
3365
|
-
runningRequiredFunds[sellAssetId] = quantizeFloat((runningRequiredFunds[sellAssetId] || 0) + signedDelta, precision);
|
|
3366
|
-
const nextPeak = Math.max(Number(peakRequiredFunds[sellAssetId] || 0), Number(runningRequiredFunds[sellAssetId] || 0));
|
|
3367
|
-
peakRequiredFunds[sellAssetId] = quantizeFloat(nextPeak, precision);
|
|
3368
|
-
}
|
|
3369
|
-
}
|
|
3370
|
-
// Calculate available funds - CRITICAL FIX: Check against FREE balance, not free+required
|
|
3371
|
-
// Bug: Previous logic added requiredFunds to available, making validation meaningless
|
|
3372
|
-
// Correct logic: available = chainFree (current free balance)
|
|
3373
|
-
// If required > available, batch will fail on execution
|
|
3374
|
-
const availableFunds = {
|
|
3375
|
-
[assetA.id]: quantizeFloat(snap.chainFreeSell || 0, assetA.precision),
|
|
3376
|
-
[assetB.id]: quantizeFloat(snap.chainFreeBuy || 0, assetB.precision)
|
|
3377
|
-
};
|
|
3378
|
-
// Check for fund violations using quantized comparison
|
|
3379
|
-
const fundViolations = [];
|
|
3380
|
-
for (const assetId in peakRequiredFunds) {
|
|
3381
|
-
const required = peakRequiredFunds[assetId];
|
|
3382
|
-
const netRequired = netRequiredFunds[assetId] || 0;
|
|
3383
|
-
const available = availableFunds[assetId] || 0;
|
|
3384
|
-
// Use precision-aware comparison
|
|
3385
|
-
const prec = (assetId === assetA.id) ? assetA.precision : assetB.precision;
|
|
3386
|
-
if (floatToBlockchainInt(required, prec) > floatToBlockchainInt(available, prec)) {
|
|
3387
|
-
fundViolations.push({
|
|
3388
|
-
asset: assetId === assetA.id ? assetA.symbol : assetB.symbol,
|
|
3389
|
-
required,
|
|
3390
|
-
netRequired,
|
|
3391
|
-
available,
|
|
3392
|
-
deficit: quantizeFloat(required - available, prec)
|
|
3393
|
-
});
|
|
3394
|
-
}
|
|
3395
|
-
}
|
|
3396
|
-
if (fundViolations.length > 0) {
|
|
3397
|
-
let summary = `[VALIDATION] Fund validation FAILED:\n`;
|
|
3398
|
-
for (const v of fundViolations) {
|
|
3399
|
-
summary += ` ${v.asset}: peakRequired=${Format.formatAmount8(v.required)}, netRequired=${Format.formatAmount8(v.netRequired)}, available=${Format.formatAmount8(v.available)}, deficit=${Format.formatAmount8(v.deficit)}\n`;
|
|
3400
|
-
}
|
|
3401
|
-
return { isValid: false, summary: summary.trim(), violations: fundViolations };
|
|
3402
|
-
}
|
|
3403
|
-
const summary = `[VALIDATION] PASSED: ${operations.length} operations`;
|
|
3404
|
-
return { isValid: true, summary };
|
|
1040
|
+
return dexbot_cow_runtime_1.default.validateOperationFunds(this, operations, assetA, assetB);
|
|
3405
1041
|
}
|
|
3406
1042
|
/**
|
|
3407
1043
|
* Resolve the ideal size from an order-like object with fallback.
|
|
@@ -3410,20 +1046,7 @@ class DEXBot {
|
|
|
3410
1046
|
* @returns {number|null} Resolved size or null
|
|
3411
1047
|
*/
|
|
3412
1048
|
_resolveIdealSizeForValidation(orderLike, fallbackSize = null) {
|
|
3413
|
-
|
|
3414
|
-
orderLike?.idealSize,
|
|
3415
|
-
orderLike?.order?.idealSize,
|
|
3416
|
-
orderLike?.size,
|
|
3417
|
-
orderLike?.order?.size,
|
|
3418
|
-
fallbackSize
|
|
3419
|
-
];
|
|
3420
|
-
for (const candidate of candidates) {
|
|
3421
|
-
const numeric = Number(candidate);
|
|
3422
|
-
if (Number.isFinite(numeric) && numeric > 0) {
|
|
3423
|
-
return numeric;
|
|
3424
|
-
}
|
|
3425
|
-
}
|
|
3426
|
-
return null;
|
|
1049
|
+
return dexbot_cow_runtime_1.default.resolveIdealSizeForValidation(this, orderLike, fallbackSize);
|
|
3427
1050
|
}
|
|
3428
1051
|
/**
|
|
3429
1052
|
* Validate that an order size is safe to execute (above minimum dust thresholds).
|
|
@@ -3434,7 +1057,7 @@ class DEXBot {
|
|
|
3434
1057
|
* @returns {import('./types').OrderValidationResult}
|
|
3435
1058
|
*/
|
|
3436
1059
|
_validateOrderSizeForExecution(size, type, orderLike = null, fallbackSize = null) {
|
|
3437
|
-
return
|
|
1060
|
+
return dexbot_cow_runtime_1.default.validateOrderSizeForExecution(this, size, type, orderLike, fallbackSize);
|
|
3438
1061
|
}
|
|
3439
1062
|
/**
|
|
3440
1063
|
* Execute a batch of order operations if the rebalance result has executable actions.
|
|
@@ -3506,12 +1129,12 @@ class DEXBot {
|
|
|
3506
1129
|
const batchEnd = Math.min(i + currentBatchSize, totalFills);
|
|
3507
1130
|
const fillBatch = fills.slice(i, batchEnd);
|
|
3508
1131
|
i = batchEnd;
|
|
3509
|
-
const batchIds = fillBatch.map(f => f.id).join(', ');
|
|
1132
|
+
const batchIds = fillBatch.map((f) => f.id).join(', ');
|
|
3510
1133
|
const label = `${contextLabel} [${batchIds}]`;
|
|
3511
1134
|
managerLog(`>>> Processing fill set ${label} (${i}/${totalFills})`, 'info');
|
|
3512
1135
|
let fullExcludeSet = excl || new Set();
|
|
3513
1136
|
if (!useUnifiedPlan) {
|
|
3514
|
-
const batchIdSet = new Set(fillBatch.map(f => f.id));
|
|
1137
|
+
const batchIdSet = new Set(fillBatch.map((f) => f.id));
|
|
3515
1138
|
fullExcludeSet = new Set(excl || []);
|
|
3516
1139
|
for (const other of fills) {
|
|
3517
1140
|
if (batchIdSet.has(other.id))
|
|
@@ -3585,17 +1208,17 @@ class DEXBot {
|
|
|
3585
1208
|
}
|
|
3586
1209
|
try {
|
|
3587
1210
|
if (this.privateKey && getKeyStore().isDaemonSigningKey(this.privateKey)) {
|
|
3588
|
-
await chainKeys.pingDaemon(this.privateKey.accountName, Math.min(TIMING.DAEMON_PING_TIMEOUT_MS, TIMING.DAEMON_STARTUP_TIMEOUT_MS), { socketPath: this.privateKey.socketPath });
|
|
1211
|
+
await chainKeys.pingDaemon(this.privateKey.accountName, Math.min(constants_1.TIMING.DAEMON_PING_TIMEOUT_MS, constants_1.TIMING.DAEMON_STARTUP_TIMEOUT_MS), { socketPath: this.privateKey.socketPath });
|
|
3589
1212
|
}
|
|
3590
1213
|
}
|
|
3591
1214
|
catch (err) {
|
|
3592
|
-
const message = `Credential daemon unavailable before ${contextLabel}: ${err
|
|
1215
|
+
const message = `Credential daemon unavailable before ${contextLabel}: ${(0, errors_1.getErrorMessage)(err)}`;
|
|
3593
1216
|
this._credentialDaemonDown = true;
|
|
3594
1217
|
this._credentialRecoveryNeeded = true;
|
|
3595
1218
|
this._suspendGridPersistenceForCredentialOutage(message);
|
|
3596
1219
|
this.manager?.logger?.log?.(`[CREDENTIAL] ${message}. Write operations paused; re-unlock with dexbot pm2.`, 'error');
|
|
3597
1220
|
const wrapped = new Error(message);
|
|
3598
|
-
wrapped.code = DAEMON_CODES.CREDENTIAL_DAEMON_UNAVAILABLE;
|
|
1221
|
+
wrapped.code = constants_1.DAEMON_CODES.CREDENTIAL_DAEMON_UNAVAILABLE;
|
|
3599
1222
|
wrapped.cause = err;
|
|
3600
1223
|
throw wrapped;
|
|
3601
1224
|
}
|
|
@@ -3608,9 +1231,9 @@ class DEXBot {
|
|
|
3608
1231
|
_isCredentialDaemonError(err) {
|
|
3609
1232
|
if (!err)
|
|
3610
1233
|
return false;
|
|
3611
|
-
if (err.code === DAEMON_CODES.CREDENTIAL_DAEMON_UNAVAILABLE)
|
|
1234
|
+
if (err.code === constants_1.DAEMON_CODES.CREDENTIAL_DAEMON_UNAVAILABLE)
|
|
3612
1235
|
return true;
|
|
3613
|
-
const message = String(err
|
|
1236
|
+
const message = String((0, errors_1.getErrorMessage)(err) || '');
|
|
3614
1237
|
return /Credential daemon|Daemon connection failed|daemon .*unavailable|dexbot-cred-daemon\.sock|ECONNREFUSED|ENOENT/.test(message);
|
|
3615
1238
|
}
|
|
3616
1239
|
/**
|
|
@@ -3626,8 +1249,8 @@ class DEXBot {
|
|
|
3626
1249
|
this.manager?.logger?.log?.('[CREDENTIAL] Deferring credential recovery until startup/broadcast activity is idle.', 'info');
|
|
3627
1250
|
this._credentialRecoveryDeferredTimer = setTimeout(() => {
|
|
3628
1251
|
this._credentialRecoveryDeferredTimer = null;
|
|
3629
|
-
this._runCredentialRecoveryAfterDaemonRestored().catch(err => {
|
|
3630
|
-
this.manager?.logger?.log?.(`[CREDENTIAL] Deferred recovery failed: ${err
|
|
1252
|
+
this._runCredentialRecoveryAfterDaemonRestored().catch((err) => {
|
|
1253
|
+
this.manager?.logger?.log?.(`[CREDENTIAL] Deferred recovery failed: ${(0, errors_1.getErrorMessage)(err)}`, 'error');
|
|
3631
1254
|
if (this.manager) {
|
|
3632
1255
|
this.manager._recoveryState = { ...this.manager._recoveryState, lastFailureAt: Date.now() };
|
|
3633
1256
|
}
|
|
@@ -3655,8 +1278,8 @@ class DEXBot {
|
|
|
3655
1278
|
}
|
|
3656
1279
|
catch (err) {
|
|
3657
1280
|
this._credentialRecoveryNeeded = true;
|
|
3658
|
-
this._suspendGridPersistenceForCredentialOutage(`credential recovery failed: ${err
|
|
3659
|
-
this.manager?.logger?.log?.(`[CREDENTIAL] Credential recovery sync failed: ${err
|
|
1281
|
+
this._suspendGridPersistenceForCredentialOutage(`credential recovery failed: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
1282
|
+
this.manager?.logger?.log?.(`[CREDENTIAL] Credential recovery sync failed: ${(0, errors_1.getErrorMessage)(err)}. Writes remain guarded by preflight.`, 'error');
|
|
3660
1283
|
}
|
|
3661
1284
|
finally {
|
|
3662
1285
|
this._credentialRecoveryInFlight = false;
|
|
@@ -3675,7 +1298,7 @@ class DEXBot {
|
|
|
3675
1298
|
this._credentialDaemonDown = false;
|
|
3676
1299
|
return;
|
|
3677
1300
|
}
|
|
3678
|
-
const intervalMs = Math.max(TIMING.DAEMON_PING_TIMEOUT_MS, TIMING.CREDENTIAL_DAEMON_WATCHDOG_MS);
|
|
1301
|
+
const intervalMs = Math.max(constants_1.TIMING.DAEMON_PING_TIMEOUT_MS, constants_1.TIMING.CREDENTIAL_DAEMON_WATCHDOG_MS);
|
|
3679
1302
|
const probe = async () => {
|
|
3680
1303
|
if (this._shuttingDown || !this._isCredentialDaemonWriteRequired())
|
|
3681
1304
|
return;
|
|
@@ -3699,7 +1322,7 @@ class DEXBot {
|
|
|
3699
1322
|
}
|
|
3700
1323
|
catch (err) {
|
|
3701
1324
|
if (!this._credentialDaemonDown) {
|
|
3702
|
-
const errMsg = String(err
|
|
1325
|
+
const errMsg = String((0, errors_1.getErrorMessage)(err) || '');
|
|
3703
1326
|
let hint = '';
|
|
3704
1327
|
if (errMsg.includes('ENOENT')) {
|
|
3705
1328
|
hint = `Socket file missing at ${token.socketPath}. The credential daemon process may have been killed (e.g. by stray Ctrl-C). Restart it with: dexbot pm2 restart dexbot-cred. If the problem persists, check the daemon log: profiles/logs/dexbot-cred.log`;
|
|
@@ -3713,10 +1336,10 @@ class DEXBot {
|
|
|
3713
1336
|
else {
|
|
3714
1337
|
hint = `Write operations will remain paused until re-unlocked with dexbot pm2.`;
|
|
3715
1338
|
}
|
|
3716
|
-
this.manager?.logger?.log?.(`[CREDENTIAL] Credential daemon watchdog failed: ${err
|
|
1339
|
+
this.manager?.logger?.log?.(`[CREDENTIAL] Credential daemon watchdog failed: ${(0, errors_1.getErrorMessage)(err)}. ${hint}`, 'error');
|
|
3717
1340
|
}
|
|
3718
1341
|
this._credentialDaemonDown = true;
|
|
3719
|
-
this._suspendGridPersistenceForCredentialOutage(`credential daemon watchdog failed: ${err
|
|
1342
|
+
this._suspendGridPersistenceForCredentialOutage(`credential daemon watchdog failed: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
3720
1343
|
}
|
|
3721
1344
|
}
|
|
3722
1345
|
finally {
|
|
@@ -3724,15 +1347,15 @@ class DEXBot {
|
|
|
3724
1347
|
}
|
|
3725
1348
|
};
|
|
3726
1349
|
this._credentialDaemonWatchdogInterval = setInterval(() => {
|
|
3727
|
-
probe().catch(err => {
|
|
3728
|
-
this.manager?.logger?.log?.(`[CREDENTIAL] Credential daemon watchdog error: ${err
|
|
1350
|
+
probe().catch((err) => {
|
|
1351
|
+
this.manager?.logger?.log?.(`[CREDENTIAL] Credential daemon watchdog error: ${(0, errors_1.getErrorMessage)(err)}`, 'warn');
|
|
3729
1352
|
});
|
|
3730
1353
|
}, intervalMs);
|
|
3731
1354
|
if (typeof this._credentialDaemonWatchdogInterval.unref === 'function') {
|
|
3732
1355
|
this._credentialDaemonWatchdogInterval.unref();
|
|
3733
1356
|
}
|
|
3734
1357
|
void probe();
|
|
3735
|
-
this._log(`Credential daemon watchdog started (${Math.round(intervalMs / TIMING.MILLISECONDS_PER_SECOND)}s interval)`);
|
|
1358
|
+
this._log(`Credential daemon watchdog started (${Math.round(intervalMs / constants_1.TIMING.MILLISECONDS_PER_SECOND)}s interval)`);
|
|
3736
1359
|
}
|
|
3737
1360
|
/**
|
|
3738
1361
|
* Stop the credential daemon watchdog interval.
|
|
@@ -3774,77 +1397,7 @@ class DEXBot {
|
|
|
3774
1397
|
* @returns {Array<{type: string, id: string, order?: Object, orderId?: string, newSize?: number, newPrice?: number, newGridId?: string}>}
|
|
3775
1398
|
*/
|
|
3776
1399
|
_buildActionsFromPlan(plan) {
|
|
3777
|
-
|
|
3778
|
-
? { ordersToPlace: plan }
|
|
3779
|
-
: (plan || {});
|
|
3780
|
-
const { ordersToPlace = [], ordersToRotate = [], ordersToUpdate = [], ordersToCancel = [] } = normalizedPlan;
|
|
3781
|
-
const actions = [];
|
|
3782
|
-
for (const o of ordersToCancel) {
|
|
3783
|
-
if (o?.orderId) {
|
|
3784
|
-
actions.push({ type: COW_ACTIONS.CANCEL, id: o.id, orderId: o.orderId });
|
|
3785
|
-
}
|
|
3786
|
-
}
|
|
3787
|
-
for (const r of ordersToRotate) {
|
|
3788
|
-
const oldOrder = r?.oldOrder || r;
|
|
3789
|
-
const id = oldOrder?.id || r?.id;
|
|
3790
|
-
const orderId = oldOrder?.orderId || r?.orderId;
|
|
3791
|
-
const newGridId = r?.newGridId || id;
|
|
3792
|
-
const newSize = Number.isFinite(Number(r?.newSize))
|
|
3793
|
-
? Number(r.newSize)
|
|
3794
|
-
: Number(r?.size || oldOrder?.size || 0);
|
|
3795
|
-
const newPrice = Number.isFinite(Number(r?.newPrice))
|
|
3796
|
-
? Number(r.newPrice)
|
|
3797
|
-
: Number(r?.price || oldOrder?.price);
|
|
3798
|
-
const orderType = r?.type || oldOrder?.type;
|
|
3799
|
-
if (!id || !orderId || !newGridId || !orderType || !Number.isFinite(newPrice) || !(newSize > 0))
|
|
3800
|
-
continue;
|
|
3801
|
-
actions.push({
|
|
3802
|
-
type: COW_ACTIONS.UPDATE,
|
|
3803
|
-
id,
|
|
3804
|
-
orderId,
|
|
3805
|
-
newGridId,
|
|
3806
|
-
newSize,
|
|
3807
|
-
newPrice,
|
|
3808
|
-
order: {
|
|
3809
|
-
id: newGridId,
|
|
3810
|
-
type: orderType,
|
|
3811
|
-
price: newPrice,
|
|
3812
|
-
size: newSize
|
|
3813
|
-
}
|
|
3814
|
-
});
|
|
3815
|
-
}
|
|
3816
|
-
for (const o of ordersToUpdate) {
|
|
3817
|
-
const partialOrder = o?.partialOrder || o;
|
|
3818
|
-
const id = o?.id || partialOrder?.id;
|
|
3819
|
-
const orderId = o?.orderId || partialOrder?.orderId;
|
|
3820
|
-
const orderType = o?.type || partialOrder?.type;
|
|
3821
|
-
const newSize = Number.isFinite(Number(o?.newSize))
|
|
3822
|
-
? Number(o.newSize)
|
|
3823
|
-
: Number(partialOrder?.size || 0);
|
|
3824
|
-
if (!id || !orderId)
|
|
3825
|
-
continue;
|
|
3826
|
-
actions.push({
|
|
3827
|
-
type: COW_ACTIONS.UPDATE,
|
|
3828
|
-
id,
|
|
3829
|
-
orderId,
|
|
3830
|
-
newSize,
|
|
3831
|
-
order: {
|
|
3832
|
-
...(partialOrder || {}),
|
|
3833
|
-
id,
|
|
3834
|
-
orderId,
|
|
3835
|
-
type: orderType,
|
|
3836
|
-
size: newSize
|
|
3837
|
-
}
|
|
3838
|
-
});
|
|
3839
|
-
}
|
|
3840
|
-
// Run CREATE actions after UPDATE actions so same-batch downsizes can
|
|
3841
|
-
// release balance before placements consume it.
|
|
3842
|
-
for (const o of ordersToPlace) {
|
|
3843
|
-
if (!o?.id)
|
|
3844
|
-
continue;
|
|
3845
|
-
actions.push({ type: COW_ACTIONS.CREATE, id: o.id, order: o });
|
|
3846
|
-
}
|
|
3847
|
-
return actions;
|
|
1400
|
+
return dexbot_cow_runtime_1.default.buildActionsFromPlan(this, plan);
|
|
3848
1401
|
}
|
|
3849
1402
|
/**
|
|
3850
1403
|
* Build a COW result object (workingGrid + actions) from a simple plan.
|
|
@@ -3852,78 +1405,7 @@ class DEXBot {
|
|
|
3852
1405
|
* @returns {{workingGrid: import('./types').WorkingGrid, workingIndexes: Object, workingBoundary: number, actions: Array}}
|
|
3853
1406
|
*/
|
|
3854
1407
|
_buildCowResultFromPlan(plan) {
|
|
3855
|
-
|
|
3856
|
-
const workingGrid = new WorkingGrid(this.manager.orders, {
|
|
3857
|
-
baseVersion: Number.isFinite(Number(this.manager._gridVersion)) ? this.manager._gridVersion : 0
|
|
3858
|
-
});
|
|
3859
|
-
const workingBoundary = this.manager.boundaryIdx;
|
|
3860
|
-
const actions = this._buildActionsFromPlan(plan);
|
|
3861
|
-
// Project planned actions into working grid so COW commit carries intended transitions.
|
|
3862
|
-
for (const action of actions) {
|
|
3863
|
-
if (action.type === COW_ACTIONS.CANCEL) {
|
|
3864
|
-
const current = workingGrid.get(action.id);
|
|
3865
|
-
if (!current)
|
|
3866
|
-
continue;
|
|
3867
|
-
workingGrid.set(action.id, convertToSpreadPlaceholder(current));
|
|
3868
|
-
}
|
|
3869
|
-
else if (action.type === COW_ACTIONS.CREATE) {
|
|
3870
|
-
if (!action.id || !action.order)
|
|
3871
|
-
continue;
|
|
3872
|
-
const current = workingGrid.get(action.id) || { id: action.id };
|
|
3873
|
-
workingGrid.set(action.id, {
|
|
3874
|
-
...current,
|
|
3875
|
-
...action.order,
|
|
3876
|
-
id: action.id,
|
|
3877
|
-
state: ORDER_STATES.VIRTUAL,
|
|
3878
|
-
orderId: null
|
|
3879
|
-
});
|
|
3880
|
-
}
|
|
3881
|
-
else if (action.type === COW_ACTIONS.UPDATE) {
|
|
3882
|
-
if (action.newGridId && action.newGridId !== action.id) {
|
|
3883
|
-
const current = workingGrid.get(action.id);
|
|
3884
|
-
if (current) {
|
|
3885
|
-
workingGrid.set(action.id, convertToSpreadPlaceholder(current));
|
|
3886
|
-
}
|
|
3887
|
-
const targetId = action.newGridId;
|
|
3888
|
-
const targetCurrent = workingGrid.get(targetId) || { id: targetId };
|
|
3889
|
-
const rotatedSize = Number.isFinite(Number(action.newSize))
|
|
3890
|
-
? Number(action.newSize)
|
|
3891
|
-
: Number(targetCurrent.size || 0);
|
|
3892
|
-
const rotatedPrice = Number.isFinite(Number(action.newPrice))
|
|
3893
|
-
? Number(action.newPrice)
|
|
3894
|
-
: Number(action.order?.price ?? targetCurrent.price);
|
|
3895
|
-
workingGrid.set(targetId, {
|
|
3896
|
-
...targetCurrent,
|
|
3897
|
-
...(action.order || {}),
|
|
3898
|
-
id: targetId,
|
|
3899
|
-
size: rotatedSize,
|
|
3900
|
-
price: rotatedPrice,
|
|
3901
|
-
state: ORDER_STATES.VIRTUAL,
|
|
3902
|
-
orderId: null
|
|
3903
|
-
});
|
|
3904
|
-
continue;
|
|
3905
|
-
}
|
|
3906
|
-
const current = workingGrid.get(action.id);
|
|
3907
|
-
if (!current)
|
|
3908
|
-
continue;
|
|
3909
|
-
const newSize = Number.isFinite(Number(action.newSize))
|
|
3910
|
-
? Number(action.newSize)
|
|
3911
|
-
: Number(current.size || 0);
|
|
3912
|
-
workingGrid.set(action.id, {
|
|
3913
|
-
...current,
|
|
3914
|
-
...(action.order || {}),
|
|
3915
|
-
id: action.id,
|
|
3916
|
-
orderId: action.orderId || current.orderId,
|
|
3917
|
-
size: newSize
|
|
3918
|
-
});
|
|
3919
|
-
}
|
|
3920
|
-
}
|
|
3921
|
-
return {
|
|
3922
|
-
workingGrid,
|
|
3923
|
-
workingIndexes: workingGrid.getIndexes(),
|
|
3924
|
-
workingBoundary,
|
|
3925
|
-
actions
|
|
3926
|
-
};
|
|
1408
|
+
return dexbot_cow_runtime_1.default.buildCowResultFromPlan(this, plan);
|
|
3927
1409
|
}
|
|
3928
1410
|
/**
|
|
3929
1411
|
* Restore skipped update slots in the working grid to master state.
|
|
@@ -3933,16 +1415,7 @@ class DEXBot {
|
|
|
3933
1415
|
* @returns {void}
|
|
3934
1416
|
*/
|
|
3935
1417
|
_restoreSkippedUpdateSlotsInWorkingGrid(workingGrid, skippedSlotIds, skippedCount = 0) {
|
|
3936
|
-
|
|
3937
|
-
return;
|
|
3938
|
-
}
|
|
3939
|
-
const masterVersion = Number.isFinite(Number(this.manager?._gridVersion))
|
|
3940
|
-
? Number(this.manager._gridVersion)
|
|
3941
|
-
: undefined;
|
|
3942
|
-
for (const slotId of skippedSlotIds) {
|
|
3943
|
-
workingGrid.syncFromMaster(this.manager.orders, slotId, masterVersion);
|
|
3944
|
-
}
|
|
3945
|
-
this.manager.logger.log(`[COW] Restored ${skippedSlotIds.size} slot(s) after ${skippedCount} skipped update action(s).`, 'debug');
|
|
1418
|
+
return dexbot_cow_runtime_1.default.restoreSkippedUpdateSlotsInWorkingGrid(this, workingGrid, skippedSlotIds, skippedCount);
|
|
3946
1419
|
}
|
|
3947
1420
|
/**
|
|
3948
1421
|
* COW broadcast: Execute blockchain operations and commit working grid on success.
|
|
@@ -3952,751 +1425,10 @@ class DEXBot {
|
|
|
3952
1425
|
* @private
|
|
3953
1426
|
*/
|
|
3954
1427
|
async _updateOrdersOnChainBatchCOW(cowResult) {
|
|
3955
|
-
|
|
3956
|
-
const { workingGrid, workingIndexes, workingBoundary, actions } = cowResult;
|
|
3957
|
-
if (this.config.dryRun) {
|
|
3958
|
-
const cancelCount = actions.filter(a => a.type === COW_ACTIONS.CANCEL).length;
|
|
3959
|
-
const createCount = actions.filter(a => a.type === COW_ACTIONS.CREATE).length;
|
|
3960
|
-
const updateCount = actions.filter(a => a.type === COW_ACTIONS.UPDATE).length;
|
|
3961
|
-
if (cancelCount > 0)
|
|
3962
|
-
this.manager.logger.log(`Dry run: would cancel ${cancelCount} orders`, 'info');
|
|
3963
|
-
if (createCount > 0)
|
|
3964
|
-
this.manager.logger.log(`Dry run: would place ${createCount} new orders`, 'info');
|
|
3965
|
-
if (updateCount > 0)
|
|
3966
|
-
this.manager.logger.log(`Dry run: would update ${updateCount} orders`, 'info');
|
|
3967
|
-
return { executed: true, hadRotation: false };
|
|
3968
|
-
}
|
|
3969
|
-
const createSlotValidation = validateCreateTargetSlots(actions, this.manager?.orders);
|
|
3970
|
-
if (!createSlotValidation.isValid) {
|
|
3971
|
-
for (const violation of createSlotValidation.violations) {
|
|
3972
|
-
this.manager.logger.log(`[COW] Rejecting CREATE for occupied slot ${violation.targetId}: ` +
|
|
3973
|
-
`existing orderId=${violation.currentOrderId}, type=${violation.currentType}, state=${violation.currentState}`, 'error');
|
|
3974
|
-
}
|
|
3975
|
-
return {
|
|
3976
|
-
executed: false,
|
|
3977
|
-
aborted: true,
|
|
3978
|
-
reason: 'CREATE_SLOT_OCCUPIED',
|
|
3979
|
-
violations: createSlotValidation.violations,
|
|
3980
|
-
hadRotation: false
|
|
3981
|
-
};
|
|
3982
|
-
}
|
|
3983
|
-
const hasCreateActions = actions.some(action => action.type === COW_ACTIONS.CREATE);
|
|
3984
|
-
// Gap 5: Recovery exhausted — block all CREATES. The bot has exhausted
|
|
3985
|
-
// its recovery attempt budget and needs a fill or sync cycle to reset.
|
|
3986
|
-
// Existing orders remain active and are monitored, but no new orders
|
|
3987
|
-
// are placed until recovery resets.
|
|
3988
|
-
if (hasCreateActions && this.manager?._recoveryExhaustedAt) {
|
|
3989
|
-
const exhaustedAge = Date.now() - this.manager._recoveryExhaustedAt;
|
|
3990
|
-
this.manager.logger.log?.(`[RECOVERY-EXHAUSTED] Blocking ${actions.filter(a => a.type === COW_ACTIONS.CREATE).length} CREATE(s) ` +
|
|
3991
|
-
`(exhausted ${(exhaustedAge / 1000).toFixed(0)}s ago). ` +
|
|
3992
|
-
`Waiting for next fill or sync cycle to reset recovery state.`, 'warn');
|
|
3993
|
-
return {
|
|
3994
|
-
executed: false,
|
|
3995
|
-
aborted: true,
|
|
3996
|
-
reason: 'RECOVERY_EXHAUSTED',
|
|
3997
|
-
hadRotation: false
|
|
3998
|
-
};
|
|
3999
|
-
}
|
|
4000
|
-
const unmatchedChainOrders = Array.isArray(this.manager?._lastUnmatchedChainOrders)
|
|
4001
|
-
? this.manager._lastUnmatchedChainOrders
|
|
4002
|
-
: [];
|
|
4003
|
-
// PENDING_BROADCASTS: an earlier batch timed out at the credential
|
|
4004
|
-
// daemon and the chain status of its CREATEs is still unknown. The
|
|
4005
|
-
// recovery path (_reconcileAfterUncertainBroadcast) is responsible
|
|
4006
|
-
// for resolving it. We refuse to publish a fresh CREATE batch until
|
|
4007
|
-
// recovery runs, otherwise we risk double-publishing or stacking
|
|
4008
|
-
// orphan orders on top of potentially-orphaned ones.
|
|
4009
|
-
const pendingBroadcasts = (this.manager && this.manager._pendingBroadcasts instanceof Map)
|
|
4010
|
-
? Array.from(this.manager._pendingBroadcasts.values())
|
|
4011
|
-
: [];
|
|
4012
|
-
if (hasCreateActions && (unmatchedChainOrders.length > 0 || pendingBroadcasts.length > 0)) {
|
|
4013
|
-
// ---- Handle pending broadcasts first ----
|
|
4014
|
-
if (pendingBroadcasts.length > 0) {
|
|
4015
|
-
this.manager.logger.log(`[COW] Rejecting CREATE batch: ${pendingBroadcasts.length} pending broadcast(s) from a prior uncertain ` +
|
|
4016
|
-
`broadcast. Running recovery before placing replacement orders.`, 'error');
|
|
4017
|
-
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
4018
|
-
if (this.manager._recoveryState)
|
|
4019
|
-
this.manager._recoveryState = { ...this.manager._recoveryState, structuralResyncRequested: true };
|
|
4020
|
-
await this.manager.requestStructuralGridResync('pending broadcasts before COW create', { pendingBroadcasts: pendingBroadcasts.map(p => p.slotId) });
|
|
4021
|
-
}
|
|
4022
|
-
try {
|
|
4023
|
-
await this._reconcileAfterUncertainBroadcast(new BroadcastUncertainError('rejected CREATE batch had pending broadcasts', {
|
|
4024
|
-
operations: pendingBroadcasts.map(p => p.order),
|
|
4025
|
-
accountName: this.account,
|
|
4026
|
-
batchId: this._currentBatchId || null,
|
|
4027
|
-
payload: null,
|
|
4028
|
-
timeoutMs: null
|
|
4029
|
-
}), []);
|
|
4030
|
-
}
|
|
4031
|
-
catch (recoverErr) {
|
|
4032
|
-
this.manager.logger.log(`[COW] Recovery from pending broadcasts failed: ${recoverErr?.message || recoverErr}`, 'error');
|
|
4033
|
-
}
|
|
4034
|
-
return {
|
|
4035
|
-
executed: false,
|
|
4036
|
-
aborted: true,
|
|
4037
|
-
reason: 'PENDING_BROADCASTS',
|
|
4038
|
-
hadRotation: false
|
|
4039
|
-
};
|
|
4040
|
-
}
|
|
4041
|
-
// ---- Unmatched orders: adopt instead of cancel ----
|
|
4042
|
-
// Unmatched orders are legitimate on-chain positions the grid
|
|
4043
|
-
// hasn't adopted yet. Cancelling them destroys capital and
|
|
4044
|
-
// creates gaps in the order book. Instead, re-sync to adopt
|
|
4045
|
-
// them into the grid, then reject (the sync invalidated the
|
|
4046
|
-
// working grid, so the existing COW plan is stale).
|
|
4047
|
-
// NOTE: syncFromOpenOrders may be a partial no-op under certain
|
|
4048
|
-
// lock conditions (e.g. _fillProcessingLock + !isReentrant).
|
|
4049
|
-
// Structural resync scheduled below handles adoption regardless.
|
|
4050
|
-
const unmatchedSample = unmatchedChainOrders
|
|
4051
|
-
.slice(0, 3)
|
|
4052
|
-
.map(o => this._formatUnmatchedChainOrderForLog(o))
|
|
4053
|
-
.join(' | ');
|
|
4054
|
-
this.manager.logger.log(`[COW] ${unmatchedChainOrders.length} unmatched chain order(s) blocking CREATES ` +
|
|
4055
|
-
(unmatchedSample ? `(${unmatchedSample})` : '') +
|
|
4056
|
-
` — adopting via sync instead of cancelling`, 'info');
|
|
4057
|
-
try {
|
|
4058
|
-
const accountRef = this.account;
|
|
4059
|
-
const freshSnapshot = await chainOrders.readOpenOrders(accountRef);
|
|
4060
|
-
if (freshSnapshot && freshSnapshot.length > 0) {
|
|
4061
|
-
const syncResult = await this.manager.syncFromOpenOrders(freshSnapshot, {
|
|
4062
|
-
skipAccounting: true,
|
|
4063
|
-
});
|
|
4064
|
-
// Only overwrite _lastUnmatchedChainOrders when sync actually
|
|
4065
|
-
// processed orders (filled + updated + corrected > 0).
|
|
4066
|
-
// Force-release and lock-contention early-exit paths return
|
|
4067
|
-
// empty arrays without touching _lastUnmatchedChainOrders —
|
|
4068
|
-
// overwriting with [] would drop stale unmatched entries.
|
|
4069
|
-
if (syncResult && Array.isArray(syncResult.unmatchedChainOrders)) {
|
|
4070
|
-
const processed = (syncResult.filledOrders?.length || 0) +
|
|
4071
|
-
(syncResult.updatedOrders?.length || 0) +
|
|
4072
|
-
(syncResult.ordersNeedingCorrection?.length || 0);
|
|
4073
|
-
if (processed > 0) {
|
|
4074
|
-
this.manager._lastUnmatchedChainOrders = syncResult.unmatchedChainOrders;
|
|
4075
|
-
this.manager.logger.log(`[COW] Adopted chain order(s) via sync: ${processed} processed, ` +
|
|
4076
|
-
`${syncResult.unmatchedChainOrders.length} still unmatched`, 'info');
|
|
4077
|
-
}
|
|
4078
|
-
else {
|
|
4079
|
-
// processed === 0 with unmatched still present means the
|
|
4080
|
-
// sync could not adopt the unmatched chain orders. This is
|
|
4081
|
-
// normal when called re-entrantly (inside _fillProcessingLock):
|
|
4082
|
-
// the sync engine runs inline and either timed out (unlikely
|
|
4083
|
-
// for a re-entrant call) or all chain orders were already
|
|
4084
|
-
// matched — leaving only stale unmatched entries.
|
|
4085
|
-
const syncUnmatchedCount = syncResult.unmatchedChainOrders.length;
|
|
4086
|
-
this.manager.logger.log(`[COW] Sync returned without processing (processed=0, ` +
|
|
4087
|
-
`unmatched=${syncUnmatchedCount} in result, ` +
|
|
4088
|
-
`_lastUnmatchedChainOrders=${unmatchedChainOrders.length}). ` +
|
|
4089
|
-
`Structural resync will handle adoption.`, syncUnmatchedCount > 0 ? 'warn' : 'debug');
|
|
4090
|
-
// If the sync result itself has unmatched entries that
|
|
4091
|
-
// differ from _lastUnmatchedChainOrders, adopt them now
|
|
4092
|
-
// so the stale tracker is more accurate for the resync.
|
|
4093
|
-
if (syncUnmatchedCount > 0 && syncUnmatchedCount !== unmatchedChainOrders.length) {
|
|
4094
|
-
this.manager._lastUnmatchedChainOrders = syncResult.unmatchedChainOrders.map((o) => ({ ...o }));
|
|
4095
|
-
this.manager.logger.log(`[COW] Updated _lastUnmatchedChainOrders from sync result: ` +
|
|
4096
|
-
`${unmatchedChainOrders.length} → ${syncUnmatchedCount}`, 'debug');
|
|
4097
|
-
}
|
|
4098
|
-
}
|
|
4099
|
-
}
|
|
4100
|
-
}
|
|
4101
|
-
}
|
|
4102
|
-
catch (syncErr) {
|
|
4103
|
-
this.manager.logger.log(`[COW] Failed to sync/unmatched orders: ${syncErr?.message || syncErr}`, 'warn');
|
|
4104
|
-
}
|
|
4105
|
-
// Working grid is stale after master grid mutation from sync.
|
|
4106
|
-
// Request structural resync to rebuild the grid on the next cycle.
|
|
4107
|
-
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
4108
|
-
if (this.manager._recoveryState)
|
|
4109
|
-
this.manager._recoveryState = { ...this.manager._recoveryState, structuralResyncRequested: true };
|
|
4110
|
-
await this.manager.requestStructuralGridResync('unmatched chain orders before COW create', { unmatchedChainOrders: unmatchedChainOrders });
|
|
4111
|
-
}
|
|
4112
|
-
this.manager.logger.log(`[COW] Rejecting CREATE batch after sync: working grid invalidated by master mutation`, 'info');
|
|
4113
|
-
return {
|
|
4114
|
-
executed: false,
|
|
4115
|
-
aborted: true,
|
|
4116
|
-
reason: 'UNMATCHED_CHAIN_ORDERS',
|
|
4117
|
-
hadRotation: false
|
|
4118
|
-
};
|
|
4119
|
-
}
|
|
4120
|
-
const { assetA, assetB } = this.manager.assets;
|
|
4121
|
-
const operations = [];
|
|
4122
|
-
const opContexts = [];
|
|
4123
|
-
const skippedUpdateSlotIds = new Set();
|
|
4124
|
-
let skippedUpdateCount = 0;
|
|
4125
|
-
// Collect IDs to lock from actions
|
|
4126
|
-
const idsToLock = new Set();
|
|
4127
|
-
for (const action of actions) {
|
|
4128
|
-
if (action.type === COW_ACTIONS.CANCEL && action.orderId) {
|
|
4129
|
-
idsToLock.add(action.orderId);
|
|
4130
|
-
if (action.id)
|
|
4131
|
-
idsToLock.add(action.id);
|
|
4132
|
-
}
|
|
4133
|
-
else if (action.type === COW_ACTIONS.CREATE && action.id) {
|
|
4134
|
-
idsToLock.add(action.id);
|
|
4135
|
-
}
|
|
4136
|
-
else if (action.type === COW_ACTIONS.UPDATE && action.orderId) {
|
|
4137
|
-
idsToLock.add(action.orderId);
|
|
4138
|
-
if (action.id)
|
|
4139
|
-
idsToLock.add(action.id);
|
|
4140
|
-
}
|
|
4141
|
-
}
|
|
4142
|
-
// Apply shadow locks
|
|
4143
|
-
this.manager.lockOrders(idsToLock);
|
|
4144
|
-
try {
|
|
4145
|
-
this._batchInFlight = true;
|
|
4146
|
-
this._markGridActivity('batch start');
|
|
4147
|
-
this.manager._setRebalanceState(REBALANCE_STATES.BROADCASTING);
|
|
4148
|
-
this.manager.startBroadcasting();
|
|
4149
|
-
// Build operations from actions
|
|
4150
|
-
for (const action of actions) {
|
|
4151
|
-
if (action.type === COW_ACTIONS.CANCEL) {
|
|
4152
|
-
try {
|
|
4153
|
-
const op = await chainOrders.buildCancelOrderOp(this.account, action.orderId);
|
|
4154
|
-
operations.push(op);
|
|
4155
|
-
const order = this.manager.orders.get(action.id) || { id: action.id, orderId: action.orderId };
|
|
4156
|
-
opContexts.push({ kind: 'cancel', order });
|
|
4157
|
-
}
|
|
4158
|
-
catch (err) {
|
|
4159
|
-
const orderNotFound = /\bnot found\b/i.test(err.message) || /\bdoes not exist\b/i.test(err.message);
|
|
4160
|
-
if (orderNotFound) {
|
|
4161
|
-
this.manager.logger.log(`[COW] Cancel skipped for ${action.id} (${action.orderId}): order already removed from chain`, 'debug');
|
|
4162
|
-
}
|
|
4163
|
-
else {
|
|
4164
|
-
this.manager.logger.log(`Failed to prepare cancel op for ${action.id}: ${err.message}`, 'error');
|
|
4165
|
-
}
|
|
4166
|
-
}
|
|
4167
|
-
}
|
|
4168
|
-
else if (action.type === COW_ACTIONS.CREATE) {
|
|
4169
|
-
try {
|
|
4170
|
-
const order = action.order;
|
|
4171
|
-
const sizeValidation = this._validateOrderSizeForExecution(order.size, order.type, order, order.size);
|
|
4172
|
-
if (!sizeValidation.isValid) {
|
|
4173
|
-
this.manager.logger.log(`Skipping create op for ${action.id}: ${sizeValidation.reason}`, 'warn');
|
|
4174
|
-
continue;
|
|
4175
|
-
}
|
|
4176
|
-
const liveSlot = this.manager.orders.get(order.id);
|
|
4177
|
-
const plannedPrice = Number(order.price);
|
|
4178
|
-
const livePrice = liveSlot ? Number(liveSlot.price) : NaN;
|
|
4179
|
-
const priceDrift = Number.isFinite(plannedPrice) && Number.isFinite(livePrice)
|
|
4180
|
-
? Math.abs(livePrice - plannedPrice)
|
|
4181
|
-
: 0;
|
|
4182
|
-
const effectiveOrder = (priceDrift > 0)
|
|
4183
|
-
? { ...order, price: livePrice, size: order.size, type: order.type }
|
|
4184
|
-
: order;
|
|
4185
|
-
if (priceDrift > 0) {
|
|
4186
|
-
this.manager.logger.log(`[COW] Pre-broadcast price freshness: slot ${order.id} ` +
|
|
4187
|
-
`drifted from planned=${plannedPrice} to live=${livePrice} ` +
|
|
4188
|
-
`(diff=${priceDrift}); rebuilding CREATE op with live price.`, 'debug');
|
|
4189
|
-
}
|
|
4190
|
-
const args = buildCreateOrderArgs(effectiveOrder, assetA, assetB);
|
|
4191
|
-
const buildResult = await chainOrders.buildCreateOrderOp(this.account, args.amountToSell, args.sellAssetId, args.minToReceive, args.receiveAssetId, null);
|
|
4192
|
-
if (!buildResult) {
|
|
4193
|
-
this.manager.logger.log(`Skipping create op for ${action.id}: amounts would round to 0 on blockchain`, 'warn');
|
|
4194
|
-
continue;
|
|
4195
|
-
}
|
|
4196
|
-
operations.push(buildResult.op);
|
|
4197
|
-
opContexts.push({ kind: 'create', id: order.id, order: effectiveOrder, args, finalInts: buildResult.finalInts });
|
|
4198
|
-
this._recordPendingBroadcast({
|
|
4199
|
-
opIndex: operations.length - 1,
|
|
4200
|
-
ctxIndex: opContexts.length - 1,
|
|
4201
|
-
order: effectiveOrder,
|
|
4202
|
-
finalInts: buildResult.finalInts
|
|
4203
|
-
});
|
|
4204
|
-
}
|
|
4205
|
-
catch (err) {
|
|
4206
|
-
this.manager.logger.log(`Failed to prepare create op for ${action.id}: ${err.message}`, 'error');
|
|
4207
|
-
}
|
|
4208
|
-
}
|
|
4209
|
-
else if (action.type === COW_ACTIONS.UPDATE) {
|
|
4210
|
-
try {
|
|
4211
|
-
// Rotation update: move existing on-chain order to a new slot/price.
|
|
4212
|
-
if (action.newGridId && action.newGridId !== action.id) {
|
|
4213
|
-
const masterOrder = this.manager.orders.get(action.id);
|
|
4214
|
-
const orderType = action.order?.type || masterOrder?.type;
|
|
4215
|
-
const newPrice = Number.isFinite(Number(action.newPrice))
|
|
4216
|
-
? Number(action.newPrice)
|
|
4217
|
-
: Number(action.order?.price);
|
|
4218
|
-
const newSize = Number.isFinite(Number(action.newSize))
|
|
4219
|
-
? Number(action.newSize)
|
|
4220
|
-
: Number(action.order?.size || 0);
|
|
4221
|
-
if (!masterOrder || !action.orderId || !orderType || !Number.isFinite(newPrice) || newSize <= 0) {
|
|
4222
|
-
continue;
|
|
4223
|
-
}
|
|
4224
|
-
const rotationSizeValidation = this._validateOrderSizeForExecution(newSize, orderType, action.order, newSize);
|
|
4225
|
-
if (!rotationSizeValidation.isValid) {
|
|
4226
|
-
this.manager.logger.log(`Skipping rotation update ${action.id} -> ${action.newGridId}: ${rotationSizeValidation.reason}`, 'warn');
|
|
4227
|
-
continue;
|
|
4228
|
-
}
|
|
4229
|
-
const { amountToSell, minToReceive } = buildCreateOrderArgs({ type: orderType, size: newSize, price: newPrice }, assetA, assetB);
|
|
4230
|
-
const buildResult = await chainOrders.buildUpdateOrderOp(this.account, action.orderId, { amountToSell, minToReceive, newPrice, orderType }, masterOrder.rawOnChain || null);
|
|
4231
|
-
if (!buildResult) {
|
|
4232
|
-
skippedUpdateCount++;
|
|
4233
|
-
if (action.id)
|
|
4234
|
-
skippedUpdateSlotIds.add(action.id);
|
|
4235
|
-
if (action.newGridId)
|
|
4236
|
-
skippedUpdateSlotIds.add(action.newGridId);
|
|
4237
|
-
this.manager.logger.log(`[COW] Skipping rotation update ${action.id} -> ${action.newGridId}: no blockchain delta`, 'debug');
|
|
4238
|
-
continue;
|
|
4239
|
-
}
|
|
4240
|
-
operations.push(buildResult.op);
|
|
4241
|
-
opContexts.push({
|
|
4242
|
-
kind: 'rotation',
|
|
4243
|
-
rotation: {
|
|
4244
|
-
oldOrder: { ...masterOrder },
|
|
4245
|
-
newGridId: action.newGridId,
|
|
4246
|
-
newPrice,
|
|
4247
|
-
newSize,
|
|
4248
|
-
type: orderType
|
|
4249
|
-
},
|
|
4250
|
-
finalInts: buildResult.finalInts
|
|
4251
|
-
});
|
|
4252
|
-
continue;
|
|
4253
|
-
}
|
|
4254
|
-
const newSize = Number.isFinite(Number(action.newSize))
|
|
4255
|
-
? Number(action.newSize)
|
|
4256
|
-
: Number(action.order?.size || 0);
|
|
4257
|
-
const masterOrder = this.manager.orders.get(action.id);
|
|
4258
|
-
const orderType = action.order?.type || masterOrder?.type;
|
|
4259
|
-
const cachedRawOnChain = masterOrder?.rawOnChain || action.order?.rawOnChain || null;
|
|
4260
|
-
const op = await chainOrders.buildUpdateOrderOp(this.account, action.orderId, { amountToSell: newSize, orderType }, cachedRawOnChain);
|
|
4261
|
-
if (!op) {
|
|
4262
|
-
skippedUpdateCount++;
|
|
4263
|
-
if (action.id)
|
|
4264
|
-
skippedUpdateSlotIds.add(action.id);
|
|
4265
|
-
if (action.newGridId)
|
|
4266
|
-
skippedUpdateSlotIds.add(action.newGridId);
|
|
4267
|
-
this.manager.logger.log(`[COW] Skipping size update ${action.id} (${action.orderId}): no blockchain delta`, 'debug');
|
|
4268
|
-
continue;
|
|
4269
|
-
}
|
|
4270
|
-
operations.push(op.op);
|
|
4271
|
-
const partialOrder = masterOrder || {
|
|
4272
|
-
id: action.id,
|
|
4273
|
-
orderId: action.orderId,
|
|
4274
|
-
type: orderType
|
|
4275
|
-
};
|
|
4276
|
-
opContexts.push({ kind: 'size-update', updateInfo: { partialOrder, newSize }, finalInts: op.finalInts });
|
|
4277
|
-
// Catch for both rotation-update and size-update branches.
|
|
4278
|
-
}
|
|
4279
|
-
catch (err) {
|
|
4280
|
-
const orderNotFound = /\bnot found\b/i.test(err.message) || /\bdoes not exist\b/i.test(err.message);
|
|
4281
|
-
if (orderNotFound) {
|
|
4282
|
-
try {
|
|
4283
|
-
const fbOrder = action.order || this.manager.orders.get(action.id);
|
|
4284
|
-
const fbType = fbOrder?.type;
|
|
4285
|
-
const fbSize = action.newSize || fbOrder?.size || 0;
|
|
4286
|
-
// Live price from TARGET slot (same pattern as CREATE path).
|
|
4287
|
-
const targetSlotId = action.newGridId || action.id;
|
|
4288
|
-
const plannedPrice = action.newPrice || action.order?.price || 0;
|
|
4289
|
-
const liveSlotForPrice = this.manager.orders.get(targetSlotId);
|
|
4290
|
-
const livePrice = liveSlotForPrice ? Number(liveSlotForPrice.price) : NaN;
|
|
4291
|
-
const priceDrift = Number.isFinite(plannedPrice) && Number.isFinite(livePrice)
|
|
4292
|
-
? Math.abs(livePrice - plannedPrice)
|
|
4293
|
-
: 0;
|
|
4294
|
-
const fbPrice = (priceDrift > 0) ? livePrice : plannedPrice;
|
|
4295
|
-
if (priceDrift > 0) {
|
|
4296
|
-
this.manager.logger.log(`[COW] CREATE fallback price drift for ${action.id} -> ${targetSlotId}: ` +
|
|
4297
|
-
`planned=${plannedPrice} live=${livePrice} (diff=${priceDrift})`, 'debug');
|
|
4298
|
-
}
|
|
4299
|
-
// Same size gate as regular CREATE path.
|
|
4300
|
-
const sizeCheck = this._validateOrderSizeForExecution(fbSize, fbType, fbOrder, fbSize);
|
|
4301
|
-
if (!sizeCheck.isValid) {
|
|
4302
|
-
this.manager.logger.log(`[COW] CREATE fallback for ${action.id} rejected by size validation: ${sizeCheck.reason}`, 'warn');
|
|
4303
|
-
}
|
|
4304
|
-
else if (fbType && fbSize > 0 && fbPrice > 0) {
|
|
4305
|
-
const fbArgs = buildCreateOrderArgs({ type: fbType, size: fbSize, price: fbPrice }, assetA, assetB);
|
|
4306
|
-
const fbResult = await chainOrders.buildCreateOrderOp(this.account, fbArgs.amountToSell, fbArgs.sellAssetId, fbArgs.minToReceive, fbArgs.receiveAssetId, null);
|
|
4307
|
-
if (fbResult) {
|
|
4308
|
-
operations.push(fbResult.op);
|
|
4309
|
-
opContexts.push({
|
|
4310
|
-
kind: 'create',
|
|
4311
|
-
id: targetSlotId,
|
|
4312
|
-
order: { id: targetSlotId, type: fbType, price: fbPrice, size: fbSize },
|
|
4313
|
-
args: { amountToSell: fbArgs.amountToSell, minToReceive: fbArgs.minToReceive },
|
|
4314
|
-
finalInts: fbResult.finalInts
|
|
4315
|
-
});
|
|
4316
|
-
this._recordPendingBroadcast({
|
|
4317
|
-
opIndex: operations.length - 1,
|
|
4318
|
-
ctxIndex: opContexts.length - 1,
|
|
4319
|
-
order: { id: targetSlotId, type: fbType, price: fbPrice, size: fbSize },
|
|
4320
|
-
finalInts: fbResult.finalInts
|
|
4321
|
-
});
|
|
4322
|
-
this.manager.logger.log(`[COW] Recovered "not found" for ${action.id}: converted UPDATE to CREATE for slot ${targetSlotId}`, 'warn');
|
|
4323
|
-
continue;
|
|
4324
|
-
}
|
|
4325
|
-
}
|
|
4326
|
-
}
|
|
4327
|
-
catch (fbErr) {
|
|
4328
|
-
this.manager.logger.log(`[COW] CREATE fallback also failed for ${action.id}: ${fbErr.message}`, 'warn');
|
|
4329
|
-
}
|
|
4330
|
-
}
|
|
4331
|
-
this.manager.logger.log(`Failed to prepare update op for ${action.id}: ${err.message}`, 'error');
|
|
4332
|
-
}
|
|
4333
|
-
}
|
|
4334
|
-
}
|
|
4335
|
-
if (skippedUpdateCount > 0) {
|
|
4336
|
-
this._restoreSkippedUpdateSlotsInWorkingGrid(workingGrid, skippedUpdateSlotIds, skippedUpdateCount);
|
|
4337
|
-
}
|
|
4338
|
-
if (operations.length === 0) {
|
|
4339
|
-
this.manager._setRebalanceState(REBALANCE_STATES.NORMAL);
|
|
4340
|
-
return { executed: false, hadRotation: false };
|
|
4341
|
-
}
|
|
4342
|
-
// Validate funds before broadcasting
|
|
4343
|
-
const validation = this._validateOperationFunds(operations, assetA, assetB);
|
|
4344
|
-
this.manager.logger.log(validation.summary, validation.isValid ? 'info' : 'warn');
|
|
4345
|
-
if (!validation.isValid) {
|
|
4346
|
-
this.manager.logger.log(`Skipping batch broadcast: ${validation.violations.length} fund violation(s) detected`, 'warn');
|
|
4347
|
-
this.manager._setRebalanceState(REBALANCE_STATES.NORMAL);
|
|
4348
|
-
return { executed: false, hadRotation: false };
|
|
4349
|
-
}
|
|
4350
|
-
await this._ensureCredentialDaemonWritable('COW batch broadcast');
|
|
4351
|
-
// Execute batch
|
|
4352
|
-
this.manager.logger.log(`[COW] Broadcasting batch with ${operations.length} operations...`, 'info');
|
|
4353
|
-
this._lastBroadcastHeartbeatAt = Date.now();
|
|
4354
|
-
const execution = await this._executeWithRetryOnUncertain(operations, opContexts);
|
|
4355
|
-
const result = execution.result;
|
|
4356
|
-
const executedContexts = execution.opContexts;
|
|
4357
|
-
// Process results and commit on success
|
|
4358
|
-
this.manager.pauseFundRecalc();
|
|
4359
|
-
try {
|
|
4360
|
-
this.manager._throwOnIllegalState = true;
|
|
4361
|
-
if (result.success) {
|
|
4362
|
-
// Pre-commit integrity: only CREATE ops require returned chainOrderIds.
|
|
4363
|
-
// Cancel/update operation results may be empty depending on the broadcaster.
|
|
4364
|
-
const preCommitResults = this._extractOperationResults(result, 'pre-commit-integrity');
|
|
4365
|
-
const missingCreateResults = this._findMissingCreateResultContexts(preCommitResults, executedContexts);
|
|
4366
|
-
if (missingCreateResults.length > 0) {
|
|
4367
|
-
const missingSlots = missingCreateResults
|
|
4368
|
-
.map(item => item.ctx?.order?.id || item.ctx?.id || `op-${item.index}`)
|
|
4369
|
-
.join(', ');
|
|
4370
|
-
this.manager.logger.log(`[COW] Refusing to commit working grid: ${missingCreateResults.length} CREATE op(s) ` +
|
|
4371
|
-
`returned no chainOrderId (${missingSlots}). Discarding working grid and syncing from chain.`, 'error');
|
|
4372
|
-
this.manager._clearWorkingGridRef();
|
|
4373
|
-
this.manager._setRebalanceState(REBALANCE_STATES.NORMAL);
|
|
4374
|
-
this._markMissingCreateResultsAsStructuralBlocker(missingCreateResults);
|
|
4375
|
-
await this._recoverAfterMissingCreateResults('missing create operation results');
|
|
4376
|
-
return {
|
|
4377
|
-
executed: false,
|
|
4378
|
-
hadRotation: false,
|
|
4379
|
-
missingCreateResults: missingCreateResults.map(item => ({
|
|
4380
|
-
index: item.index,
|
|
4381
|
-
slotId: item.ctx?.order?.id || item.ctx?.id || null
|
|
4382
|
-
}))
|
|
4383
|
-
};
|
|
4384
|
-
}
|
|
4385
|
-
// SUCCESS: Commit working grid to master (atomic swap)
|
|
4386
|
-
this.manager.logger.log('[COW] Blockchain success - committing working grid to master', 'info');
|
|
4387
|
-
// RC-FIX: skipRecalc prevents invariant violation before optimistic accounting
|
|
4388
|
-
await this.manager._commitWorkingGrid(workingGrid, workingIndexes, workingBoundary, { skipRecalc: true });
|
|
4389
|
-
// Commitment accounting is handled in real-time by
|
|
4390
|
-
// updateOptimisticFreeBalance when capital is committed to orders.
|
|
4391
|
-
// The old post-batch deduction path was removed to avoid double-counting.
|
|
4392
|
-
// Process batch results for logging/metrics
|
|
4393
|
-
const batchResult = await this._processBatchResults(result, executedContexts);
|
|
4394
|
-
// Persist to disk. CRITICAL: working grid was already committed
|
|
4395
|
-
// to master above; if persistence is skipped or validation fails
|
|
4396
|
-
// here, the on-disk snapshot will be older than the in-memory
|
|
4397
|
-
// master. Retry once and surface the failure explicitly so the
|
|
4398
|
-
// next cycle / shutdown can recover.
|
|
4399
|
-
const persistResult = await this.manager.persistGrid();
|
|
4400
|
-
if (persistResult && (persistResult.skipped || persistResult.isValid === false)) {
|
|
4401
|
-
this.manager.logger.log(`[COW][PERSIST-GUARD] First persist attempt was ` +
|
|
4402
|
-
`${persistResult.skipped ? 'skipped' : 'invalid'} ` +
|
|
4403
|
-
`(${persistResult.reason || 'no reason'}); retrying once before ` +
|
|
4404
|
-
`clearing working grid reference.`, 'warn');
|
|
4405
|
-
this.manager._persistenceWarning = persistResult;
|
|
4406
|
-
const retryResult = await this.manager.persistGrid();
|
|
4407
|
-
if (retryResult && (retryResult.skipped || retryResult.isValid === false)) {
|
|
4408
|
-
this.manager.logger.log(`[COW][PERSIST-GUARD] Retry also skipped/invalid ` +
|
|
4409
|
-
`(${retryResult.reason || 'no reason'}). Master grid in memory ` +
|
|
4410
|
-
`is ahead of disk snapshot; structural resync requested.`, 'error');
|
|
4411
|
-
if (typeof this.manager.requestStructuralGridResync === 'function') {
|
|
4412
|
-
this.manager._recoveryState = { ...this.manager._recoveryState, structuralResyncRequested: true };
|
|
4413
|
-
await this.manager.requestStructuralGridResync('persistence guard triggered after COW batch', { persistReason: retryResult.reason || 'unknown' });
|
|
4414
|
-
}
|
|
4415
|
-
}
|
|
4416
|
-
else {
|
|
4417
|
-
delete this.manager._persistenceWarning;
|
|
4418
|
-
}
|
|
4419
|
-
}
|
|
4420
|
-
else if (this.manager._persistenceWarning) {
|
|
4421
|
-
delete this.manager._persistenceWarning;
|
|
4422
|
-
}
|
|
4423
|
-
this._metrics.batchesExecuted++;
|
|
4424
|
-
this.manager._clearWorkingGridRef();
|
|
4425
|
-
this._clearPendingBroadcasts();
|
|
4426
|
-
return { executed: true, hadRotation: true, ...batchResult };
|
|
4427
|
-
}
|
|
4428
|
-
else {
|
|
4429
|
-
// FAILURE: Working grid discarded, master unchanged
|
|
4430
|
-
this.manager.logger.log('[COW] Blockchain failed - working grid discarded, master unchanged', 'warn');
|
|
4431
|
-
this.manager._clearWorkingGridRef();
|
|
4432
|
-
this._clearPendingBroadcasts();
|
|
4433
|
-
return { executed: false, hadRotation: false, ...result };
|
|
4434
|
-
}
|
|
4435
|
-
}
|
|
4436
|
-
finally {
|
|
4437
|
-
this.manager._throwOnIllegalState = false;
|
|
4438
|
-
// Keep broadcasting true during resumeFundRecalc to skip invariant checks
|
|
4439
|
-
// that would fail due to stale accountTotals (not yet refreshed from blockchain)
|
|
4440
|
-
await this.manager.resumeFundRecalc();
|
|
4441
|
-
this.manager.stopBroadcasting();
|
|
4442
|
-
const createCount = actions.filter(a => a.type === COW_ACTIONS.CREATE).length;
|
|
4443
|
-
const cancelCount = actions.filter(a => a.type === COW_ACTIONS.CANCEL).length;
|
|
4444
|
-
this.manager.logger.logFundsStatus(this.manager, `AFTER COW batch (created=${createCount}, cancelled=${cancelCount})`);
|
|
4445
|
-
}
|
|
4446
|
-
}
|
|
4447
|
-
catch (err) {
|
|
4448
|
-
this.manager.logger.log(`[COW] Batch transaction failed: ${err.message}`, 'error');
|
|
4449
|
-
if (err?.partialOnChainState) {
|
|
4450
|
-
this.manager.logger.log(`[COW] Non-atomic grouped execution detected (${err.groupsBroadcast}/${err.groupsTotal} groups broadcast). Local rollback cannot undo confirmed on-chain operations; next sync/reconcile will converge state.`, 'warn');
|
|
4451
|
-
}
|
|
4452
|
-
this.manager.stopBroadcasting();
|
|
4453
|
-
this.manager._clearWorkingGridRef();
|
|
4454
|
-
// BROADCAST_UNCERTAIN: the credential daemon timed out (or hit its
|
|
4455
|
-
// inner deadline) and the chain status of the planned CREATEs is
|
|
4456
|
-
// unknown. Run the reconcile-then-decide recovery path: read the
|
|
4457
|
-
// chain, match each pending broadcast by fingerprint, adopt any
|
|
4458
|
-
// chain-side matches, and discard the rest. We MUST NOT throw —
|
|
4459
|
-
// throwing would re-enter the catch loop on the next attempt and
|
|
4460
|
-
// potentially double-publish.
|
|
4461
|
-
if (err instanceof BroadcastUncertainError) {
|
|
4462
|
-
return await this._reconcileAfterUncertainBroadcast(err, opContexts);
|
|
4463
|
-
}
|
|
4464
|
-
// Handle hard abort
|
|
4465
|
-
const hardAbortResult = await this._handleBatchHardAbort(err, 'COW batch processing', operations.length);
|
|
4466
|
-
if (hardAbortResult)
|
|
4467
|
-
return hardAbortResult;
|
|
4468
|
-
// Check for stale orders — filled in the ~1.5s broadcast window after our plan was built.
|
|
4469
|
-
const staleOrderIds = new Set();
|
|
4470
|
-
const patterns = [
|
|
4471
|
-
/Limit order (1\.7\.\d+) does not exist/g,
|
|
4472
|
-
/Unable to find Object (1\.7\.\d+)/g,
|
|
4473
|
-
/object (1\.7\.\d+) (?:does not exist|not found)/gi
|
|
4474
|
-
];
|
|
4475
|
-
for (const pattern of patterns) {
|
|
4476
|
-
let m;
|
|
4477
|
-
while ((m = pattern.exec(err.message)) !== null) {
|
|
4478
|
-
staleOrderIds.add(m[1]);
|
|
4479
|
-
}
|
|
4480
|
-
}
|
|
4481
|
-
// "Cannot deduct all or more from order than order contains" means the order still
|
|
4482
|
-
// exists, but its on-chain size shrank during the broadcast window. That is not an
|
|
4483
|
-
// explicit stale/missing-order signal, so reconcile from chain instead of virtualizing.
|
|
4484
|
-
if (/Cannot deduct all or more from order than order contains/.test(err.message)) {
|
|
4485
|
-
return await this._recoverBatchSizeDrift(err, opContexts);
|
|
4486
|
-
}
|
|
4487
|
-
if (staleOrderIds.size > 0) {
|
|
4488
|
-
// Recover explicit missing-order failures without aborting the entire fill cycle.
|
|
4489
|
-
// Use the manager update path so indexes, working-grid sync, accounting, and
|
|
4490
|
-
// persistence stay coherent.
|
|
4491
|
-
return await this._recoverExplicitStaleOrders(staleOrderIds, 'cow-stale-order-cleanup');
|
|
4492
|
-
}
|
|
4493
|
-
throw err;
|
|
4494
|
-
}
|
|
4495
|
-
finally {
|
|
4496
|
-
this._batchInFlight = false;
|
|
4497
|
-
this._markGridActivity('batch end');
|
|
4498
|
-
this.manager.unlockOrders(idsToLock);
|
|
4499
|
-
if (!this._shuttingDown && this._incomingFillQueue.length > 0) {
|
|
4500
|
-
this._scheduleFillConsumerRestart(chainOrders);
|
|
4501
|
-
}
|
|
4502
|
-
}
|
|
1428
|
+
return dexbot_cow_runtime_1.default.updateOrdersOnChainBatchCOW(this, cowResult);
|
|
4503
1429
|
}
|
|
4504
|
-
/**
|
|
4505
|
-
* Process results from batch transaction execution.
|
|
4506
|
-
* Updates order state, synchronizes with chain, and deducts BTS fees.
|
|
4507
|
-
* @param {Object} result - Transaction result from executeBatch
|
|
4508
|
-
* @param {Array} opContexts - Operation context array with operation metadata (must be 1:1 with result.operation_results)
|
|
4509
|
-
* @returns {Object} Result with { executed: boolean, hadRotation: boolean }
|
|
4510
|
-
* @private
|
|
4511
|
-
*/
|
|
4512
1430
|
async _processBatchResults(result, opContexts) {
|
|
4513
|
-
|
|
4514
|
-
const { getAssetFees } = require('./order/utils/math');
|
|
4515
|
-
// IMPORTANT: Call without amount to get fee schedule fields
|
|
4516
|
-
// ({ createFee, updateFee, ... }), not proceeds projection fields.
|
|
4517
|
-
const btsFeeData = getAssetFees('BTS');
|
|
4518
|
-
let hadRotation = false;
|
|
4519
|
-
let updateOperationCount = 0;
|
|
4520
|
-
const updatesToApply = [];
|
|
4521
|
-
for (let i = 0; i < opContexts.length; i++) {
|
|
4522
|
-
const ctx = opContexts[i];
|
|
4523
|
-
const res = results[i];
|
|
4524
|
-
if (ctx.kind === 'cancel') {
|
|
4525
|
-
this.manager.logger.log(`Cancelled surplus order ${ctx.order.id} (${ctx.order.orderId})`, 'info');
|
|
4526
|
-
const oldOrder = ctx.order;
|
|
4527
|
-
const committedOrder = oldOrder?.id ? this.manager.orders.get(oldOrder.id) : null;
|
|
4528
|
-
// The COW commit already updated manager.orders. Apply ONLY the optimistic
|
|
4529
|
-
// accounting transition using pre-commit -> committed order states.
|
|
4530
|
-
if (oldOrder && committedOrder && this.manager.accountant) {
|
|
4531
|
-
await this.manager.accountant.updateOptimisticFreeBalance(oldOrder, committedOrder, 'fill-cancel', btsFeeData?.cancelFee || 0, false);
|
|
4532
|
-
}
|
|
4533
|
-
}
|
|
4534
|
-
else if (ctx.kind === 'size-update') {
|
|
4535
|
-
const oldOrder = ctx.updateInfo.partialOrder;
|
|
4536
|
-
const ord = this.manager.orders.get(oldOrder.id);
|
|
4537
|
-
// Apply optimistic accounting from pre-commit -> committed state,
|
|
4538
|
-
// including blockchain update fee deduction.
|
|
4539
|
-
if (oldOrder && ord && this.manager.accountant) {
|
|
4540
|
-
await this.manager.accountant.updateOptimisticFreeBalance(oldOrder, ord, 'order-update', btsFeeData.updateFee, false);
|
|
4541
|
-
}
|
|
4542
|
-
if (ord) {
|
|
4543
|
-
const updatedSlot = { ...ord, size: ctx.updateInfo.newSize };
|
|
4544
|
-
// Update rawOnChain cache with new integers
|
|
4545
|
-
if (ctx.finalInts) {
|
|
4546
|
-
updatedSlot.rawOnChain = {
|
|
4547
|
-
id: ord.orderId,
|
|
4548
|
-
for_sale: String(ctx.finalInts.sell),
|
|
4549
|
-
sell_price: {
|
|
4550
|
-
base: { amount: String(ctx.finalInts.sell), asset_id: ctx.finalInts.sellAssetId },
|
|
4551
|
-
quote: { amount: String(ctx.finalInts.receive), asset_id: ctx.finalInts.receiveAssetId }
|
|
4552
|
-
}
|
|
4553
|
-
};
|
|
4554
|
-
}
|
|
4555
|
-
updatesToApply.push({ order: updatedSlot, context: 'post-update-metadata' });
|
|
4556
|
-
}
|
|
4557
|
-
this.manager.logger.log(`Size update complete: ${ctx.updateInfo.partialOrder.orderId}`, 'info');
|
|
4558
|
-
updateOperationCount++;
|
|
4559
|
-
}
|
|
4560
|
-
else if (ctx.kind === 'create') {
|
|
4561
|
-
const chainOrderId = res && res[1];
|
|
4562
|
-
if (chainOrderId) {
|
|
4563
|
-
// synchronizeWithChain handles the full VIRTUAL -> ACTIVE transition
|
|
4564
|
-
// including orderId assignment and fee deduction.
|
|
4565
|
-
await this.manager.synchronizeWithChain({
|
|
4566
|
-
gridOrderId: ctx.order.id, chainOrderId, expectedType: ctx.order.type, fee: btsFeeData.createFee
|
|
4567
|
-
}, 'createOrder');
|
|
4568
|
-
// After sync, apply rawOnChain metadata if available
|
|
4569
|
-
if (ctx.finalInts) {
|
|
4570
|
-
const syncedOrder = this.manager.orders.get(ctx.order.id);
|
|
4571
|
-
if (syncedOrder) {
|
|
4572
|
-
updatesToApply.push({
|
|
4573
|
-
order: {
|
|
4574
|
-
...syncedOrder,
|
|
4575
|
-
rawOnChain: {
|
|
4576
|
-
id: chainOrderId,
|
|
4577
|
-
for_sale: String(ctx.finalInts.sell),
|
|
4578
|
-
sell_price: {
|
|
4579
|
-
base: { amount: String(ctx.finalInts.sell), asset_id: ctx.finalInts.sellAssetId },
|
|
4580
|
-
quote: { amount: String(ctx.finalInts.receive), asset_id: ctx.finalInts.receiveAssetId }
|
|
4581
|
-
}
|
|
4582
|
-
}
|
|
4583
|
-
},
|
|
4584
|
-
context: 'post-placement-metadata'
|
|
4585
|
-
});
|
|
4586
|
-
}
|
|
4587
|
-
}
|
|
4588
|
-
this.manager.logger.log(`Placed ${ctx.order.type} order ${ctx.order.id} -> ${chainOrderId}`, 'info');
|
|
4589
|
-
}
|
|
4590
|
-
else {
|
|
4591
|
-
const fingerprint = [
|
|
4592
|
-
`type=${ctx.order.type || 'unknown'}`,
|
|
4593
|
-
`price=${Format.formatPrice6(ctx.order.price)}`,
|
|
4594
|
-
`size=${Format.formatAmount(ctx.order.size)}`
|
|
4595
|
-
].join(',');
|
|
4596
|
-
this.manager.logger.log(`[COW] CRITICAL: Create op for slot ${ctx.order.id} (type=${ctx.order.type}) ` +
|
|
4597
|
-
`returned no chainOrderId. Identify any orphaned on-chain order by local fingerprint ` +
|
|
4598
|
-
`${fingerprint} before cancelling.`, 'error');
|
|
4599
|
-
}
|
|
4600
|
-
}
|
|
4601
|
-
else if (ctx.kind === 'rotation') {
|
|
4602
|
-
hadRotation = true;
|
|
4603
|
-
const { rotation } = ctx;
|
|
4604
|
-
const { oldOrder, newPrice, newGridId, newSize, type } = rotation;
|
|
4605
|
-
if (!newGridId) {
|
|
4606
|
-
// Size correction only
|
|
4607
|
-
const ord = this.manager.orders.get(oldOrder.id || rotation.id);
|
|
4608
|
-
// Apply optimistic accounting from pre-commit -> committed state,
|
|
4609
|
-
// including blockchain update fee deduction.
|
|
4610
|
-
if (oldOrder && ord && this.manager.accountant) {
|
|
4611
|
-
await this.manager.accountant.updateOptimisticFreeBalance(oldOrder, ord, 'order-update', btsFeeData.updateFee, false);
|
|
4612
|
-
}
|
|
4613
|
-
if (ord) {
|
|
4614
|
-
const updatedSlot = { ...ord, size: newSize };
|
|
4615
|
-
// Update rawOnChain cache with new integers
|
|
4616
|
-
if (ctx.finalInts) {
|
|
4617
|
-
updatedSlot.rawOnChain = {
|
|
4618
|
-
id: ord.orderId,
|
|
4619
|
-
for_sale: String(ctx.finalInts.sell),
|
|
4620
|
-
sell_price: {
|
|
4621
|
-
base: { amount: String(ctx.finalInts.sell), asset_id: ctx.finalInts.sellAssetId },
|
|
4622
|
-
quote: { amount: String(ctx.finalInts.receive), asset_id: ctx.finalInts.receiveAssetId }
|
|
4623
|
-
}
|
|
4624
|
-
};
|
|
4625
|
-
}
|
|
4626
|
-
updatesToApply.push({ order: updatedSlot, context: 'post-update-metadata' });
|
|
4627
|
-
}
|
|
4628
|
-
updateOperationCount++;
|
|
4629
|
-
continue;
|
|
4630
|
-
}
|
|
4631
|
-
// Full rotation: old slot was virtualized in the committed working grid.
|
|
4632
|
-
// Activate the destination slot with the existing on-chain orderId.
|
|
4633
|
-
const slot = this.manager.orders.get(newGridId);
|
|
4634
|
-
if (!slot) {
|
|
4635
|
-
this.manager.logger.log(`[ROTATION] Destination slot ${newGridId} missing from master grid after COW commit - skipping activation, sync will reconcile`, 'error');
|
|
4636
|
-
// Still clear source orderId to prevent a stale chain reference persisting
|
|
4637
|
-
if (oldOrder?.id && oldOrder.id !== newGridId) {
|
|
4638
|
-
const staleSource = this.manager.orders.get(oldOrder.id);
|
|
4639
|
-
if (staleSource?.orderId) {
|
|
4640
|
-
updatesToApply.push({
|
|
4641
|
-
order: { ...staleSource, state: ORDER_STATES.VIRTUAL, orderId: null, rawOnChain: null },
|
|
4642
|
-
context: 'post-rotation-source-clear'
|
|
4643
|
-
});
|
|
4644
|
-
}
|
|
4645
|
-
}
|
|
4646
|
-
continue;
|
|
4647
|
-
}
|
|
4648
|
-
const updatedSlot = {
|
|
4649
|
-
...slot,
|
|
4650
|
-
id: newGridId,
|
|
4651
|
-
type,
|
|
4652
|
-
size: newSize,
|
|
4653
|
-
price: newPrice,
|
|
4654
|
-
state: ORDER_STATES.ACTIVE,
|
|
4655
|
-
orderId: oldOrder?.orderId || slot.orderId || null
|
|
4656
|
-
};
|
|
4657
|
-
if (ctx.finalInts) {
|
|
4658
|
-
updatedSlot.rawOnChain = {
|
|
4659
|
-
id: updatedSlot.orderId,
|
|
4660
|
-
for_sale: String(ctx.finalInts.sell),
|
|
4661
|
-
sell_price: {
|
|
4662
|
-
base: { amount: String(ctx.finalInts.sell), asset_id: ctx.finalInts.sellAssetId },
|
|
4663
|
-
quote: { amount: String(ctx.finalInts.receive), asset_id: ctx.finalInts.receiveAssetId }
|
|
4664
|
-
}
|
|
4665
|
-
};
|
|
4666
|
-
}
|
|
4667
|
-
if (oldOrder && updatedSlot && this.manager.accountant) {
|
|
4668
|
-
await this.manager.accountant.updateOptimisticFreeBalance(oldOrder, updatedSlot, 'order-update', btsFeeData.updateFee, false);
|
|
4669
|
-
}
|
|
4670
|
-
// Ensure rotation source slot is cleared in master state.
|
|
4671
|
-
// COW projection may keep source slots ACTIVE when target keeps a non-zero
|
|
4672
|
-
// virtual size, but after a successful on-chain rotation the source orderId
|
|
4673
|
-
// must no longer remain attached to the old slot.
|
|
4674
|
-
if (oldOrder?.id && oldOrder.id !== newGridId) {
|
|
4675
|
-
const currentSource = this.manager.orders.get(oldOrder.id);
|
|
4676
|
-
if (currentSource && currentSource.orderId) {
|
|
4677
|
-
updatesToApply.push({
|
|
4678
|
-
order: {
|
|
4679
|
-
...currentSource,
|
|
4680
|
-
state: ORDER_STATES.VIRTUAL,
|
|
4681
|
-
orderId: null,
|
|
4682
|
-
rawOnChain: null
|
|
4683
|
-
},
|
|
4684
|
-
context: 'post-rotation-source-clear'
|
|
4685
|
-
});
|
|
4686
|
-
}
|
|
4687
|
-
}
|
|
4688
|
-
updatesToApply.push({ order: updatedSlot, context: 'post-rotation-metadata' });
|
|
4689
|
-
}
|
|
4690
|
-
}
|
|
4691
|
-
// Apply all collected updates in a single batch
|
|
4692
|
-
if (updatesToApply.length > 0) {
|
|
4693
|
-
await this.manager.applyGridUpdateBatch(updatesToApply.map(u => u.order), 'batch-results-process', { skipAccounting: true });
|
|
4694
|
-
}
|
|
4695
|
-
return {
|
|
4696
|
-
executed: true,
|
|
4697
|
-
hadRotation,
|
|
4698
|
-
updateOperationCount
|
|
4699
|
-
};
|
|
1431
|
+
return dexbot_cow_runtime_1.default.processBatchResults(this, result, opContexts);
|
|
4700
1432
|
}
|
|
4701
1433
|
/**
|
|
4702
1434
|
* Perform grid recalculation triggered by trigger file.
|
|
@@ -4707,7 +1439,7 @@ class DEXBot {
|
|
|
4707
1439
|
* @private
|
|
4708
1440
|
*/
|
|
4709
1441
|
async _performGridResync(options = {}) {
|
|
4710
|
-
return
|
|
1442
|
+
return dexbot_maintenance_runtime_1.default.performGridResync(this, options);
|
|
4711
1443
|
}
|
|
4712
1444
|
/**
|
|
4713
1445
|
* Handle any pending trigger file reset at startup.
|
|
@@ -4716,7 +1448,7 @@ class DEXBot {
|
|
|
4716
1448
|
* @private
|
|
4717
1449
|
*/
|
|
4718
1450
|
async _handlePendingTriggerReset() {
|
|
4719
|
-
return
|
|
1451
|
+
return dexbot_maintenance_runtime_1.default.handlePendingTriggerReset(this);
|
|
4720
1452
|
}
|
|
4721
1453
|
/**
|
|
4722
1454
|
* Setup trigger file detection for grid reset.
|
|
@@ -4724,7 +1456,7 @@ class DEXBot {
|
|
|
4724
1456
|
* @private
|
|
4725
1457
|
*/
|
|
4726
1458
|
async _setupTriggerFileDetection() {
|
|
4727
|
-
return
|
|
1459
|
+
return dexbot_maintenance_runtime_1.default.setupTriggerFileDetection(this);
|
|
4728
1460
|
}
|
|
4729
1461
|
/**
|
|
4730
1462
|
* Starts the bot's operation.
|
|
@@ -4743,14 +1475,14 @@ class DEXBot {
|
|
|
4743
1475
|
*/
|
|
4744
1476
|
async startWithPrivateKey(privateKey) {
|
|
4745
1477
|
// Initialize account data with provided private key
|
|
4746
|
-
await waitForConnected(TIMING.CONNECTION_TIMEOUT_MS);
|
|
1478
|
+
await waitForConnected(constants_1.TIMING.CONNECTION_TIMEOUT_MS);
|
|
4747
1479
|
if (this.config && this.config.preferredAccount) {
|
|
4748
1480
|
try {
|
|
4749
1481
|
this.privateKey = privateKey;
|
|
4750
1482
|
await this._setupAccountContext(this.config.preferredAccount);
|
|
4751
1483
|
}
|
|
4752
1484
|
catch (err) {
|
|
4753
|
-
this._warn(`Auto-selection of preferredAccount failed: ${err
|
|
1485
|
+
this._warn(`Auto-selection of preferredAccount failed: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
4754
1486
|
throw err;
|
|
4755
1487
|
}
|
|
4756
1488
|
}
|
|
@@ -4773,7 +1505,7 @@ class DEXBot {
|
|
|
4773
1505
|
await this._finishStartupSequence(startupState);
|
|
4774
1506
|
}
|
|
4775
1507
|
catch (err) {
|
|
4776
|
-
this._warn(`Error during grid initialization: ${err
|
|
1508
|
+
this._warn(`Error during grid initialization: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
4777
1509
|
await this.shutdown();
|
|
4778
1510
|
throw err;
|
|
4779
1511
|
}
|
|
@@ -4796,10 +1528,10 @@ class DEXBot {
|
|
|
4796
1528
|
* @private
|
|
4797
1529
|
*/
|
|
4798
1530
|
async _performPeriodicGridChecks() {
|
|
4799
|
-
return
|
|
1531
|
+
return dexbot_maintenance_runtime_1.default.performPeriodicGridChecks(this);
|
|
4800
1532
|
}
|
|
4801
1533
|
_isOpenOrdersSyncLoopEnabled() {
|
|
4802
|
-
return
|
|
1534
|
+
return dexbot_maintenance_runtime_1.default.isOpenOrdersSyncLoopEnabled(this);
|
|
4803
1535
|
}
|
|
4804
1536
|
/**
|
|
4805
1537
|
* Start the open-orders watchdog sync loop.
|
|
@@ -4807,14 +1539,14 @@ class DEXBot {
|
|
|
4807
1539
|
* @private
|
|
4808
1540
|
*/
|
|
4809
1541
|
_startOpenOrdersSyncLoop() {
|
|
4810
|
-
return
|
|
1542
|
+
return dexbot_maintenance_runtime_1.default.startOpenOrdersSyncLoop(this);
|
|
4811
1543
|
}
|
|
4812
1544
|
/**
|
|
4813
1545
|
* Stop the open-orders watchdog sync loop.
|
|
4814
1546
|
* @private
|
|
4815
1547
|
*/
|
|
4816
1548
|
async _stopOpenOrdersSyncLoop() {
|
|
4817
|
-
return
|
|
1549
|
+
return dexbot_maintenance_runtime_1.default.stopOpenOrdersSyncLoop(this);
|
|
4818
1550
|
}
|
|
4819
1551
|
/**
|
|
4820
1552
|
* Set up periodic blockchain account balance fetch interval.
|
|
@@ -4822,17 +1554,17 @@ class DEXBot {
|
|
|
4822
1554
|
* @private
|
|
4823
1555
|
*/
|
|
4824
1556
|
_setupBlockchainFetchInterval() {
|
|
4825
|
-
return
|
|
1557
|
+
return dexbot_maintenance_runtime_1.default.setupBlockchainFetchInterval(this);
|
|
4826
1558
|
}
|
|
4827
1559
|
/**
|
|
4828
1560
|
* Stop the periodic blockchain fetch interval.
|
|
4829
1561
|
* @private
|
|
4830
1562
|
*/
|
|
4831
1563
|
_stopBlockchainFetchInterval() {
|
|
4832
|
-
return
|
|
1564
|
+
return dexbot_maintenance_runtime_1.default.stopBlockchainFetchInterval(this);
|
|
4833
1565
|
}
|
|
4834
1566
|
async _releaseMarketAdapterRuntime(context = 'shutdown') {
|
|
4835
|
-
return
|
|
1567
|
+
return dexbot_maintenance_runtime_1.default.releaseMarketAdapterRuntime(this, this.config?.botKey || this.config?.name, context);
|
|
4836
1568
|
}
|
|
4837
1569
|
/**
|
|
4838
1570
|
* Get or create the credit runtime for debt policy management.
|
|
@@ -4848,8 +1580,8 @@ class DEXBot {
|
|
|
4848
1580
|
return null;
|
|
4849
1581
|
}
|
|
4850
1582
|
if (!this._creditRuntime) {
|
|
4851
|
-
this._creditRuntime = new
|
|
4852
|
-
stateDir: PATHS.CREDIT_RUNTIME_DIR,
|
|
1583
|
+
this._creditRuntime = new credit_runtime_1.default(this, {
|
|
1584
|
+
stateDir: paths_1.PATHS.CREDIT_RUNTIME_DIR,
|
|
4853
1585
|
});
|
|
4854
1586
|
}
|
|
4855
1587
|
return this._creditRuntime;
|
|
@@ -4878,7 +1610,7 @@ class DEXBot {
|
|
|
4878
1610
|
await runtime.refreshState();
|
|
4879
1611
|
}
|
|
4880
1612
|
catch (err) {
|
|
4881
|
-
this._warn(`Credit runtime refresh/sync failed: ${err
|
|
1613
|
+
this._warn(`Credit runtime refresh/sync failed: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
4882
1614
|
}
|
|
4883
1615
|
}
|
|
4884
1616
|
/**
|
|
@@ -4903,7 +1635,7 @@ class DEXBot {
|
|
|
4903
1635
|
if (!runtime) {
|
|
4904
1636
|
return;
|
|
4905
1637
|
}
|
|
4906
|
-
const intervalMin = Number(this.config?.TIMING?.CREDIT_DEAL_CHECK_INTERVAL_MIN ?? TIMING.CREDIT_DEAL_CHECK_INTERVAL_MIN);
|
|
1638
|
+
const intervalMin = Number(this.config?.TIMING?.CREDIT_DEAL_CHECK_INTERVAL_MIN ?? constants_1.TIMING.CREDIT_DEAL_CHECK_INTERVAL_MIN);
|
|
4907
1639
|
if (!Number.isFinite(intervalMin) || intervalMin <= 0) {
|
|
4908
1640
|
this._log('Credit deal watchdog disabled by configuration (TIMING.CREDIT_DEAL_CHECK_INTERVAL_MIN <= 0)');
|
|
4909
1641
|
return;
|
|
@@ -4912,13 +1644,13 @@ class DEXBot {
|
|
|
4912
1644
|
clearInterval(this._creditWatchdogInterval);
|
|
4913
1645
|
this._creditWatchdogInterval = null;
|
|
4914
1646
|
}
|
|
4915
|
-
const intervalMs = intervalMin * 60 * TIMING.MILLISECONDS_PER_SECOND;
|
|
1647
|
+
const intervalMs = intervalMin * 60 * constants_1.TIMING.MILLISECONDS_PER_SECOND;
|
|
4916
1648
|
this._creditWatchdogInterval = setInterval(async () => {
|
|
4917
1649
|
try {
|
|
4918
1650
|
await runtime.runCreditWatchdog();
|
|
4919
1651
|
}
|
|
4920
1652
|
catch (err) {
|
|
4921
|
-
this._warn(`Credit watchdog error: ${err
|
|
1653
|
+
this._warn(`Credit watchdog error: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
4922
1654
|
}
|
|
4923
1655
|
}, intervalMs);
|
|
4924
1656
|
if (typeof this._creditWatchdogInterval?.unref === 'function') {
|
|
@@ -4937,89 +1669,17 @@ class DEXBot {
|
|
|
4937
1669
|
}
|
|
4938
1670
|
}
|
|
4939
1671
|
async requestGridReset(reason = 'structural change', options = {}) {
|
|
4940
|
-
|
|
4941
|
-
return { skipped: true, reason: 'grid resync unavailable' };
|
|
4942
|
-
}
|
|
4943
|
-
const message = reason ? `[CR-RESET] ${reason}` : '[CR-RESET] grid reset requested';
|
|
4944
|
-
this._log(`${message}; rebuilding grid from fresh on-chain state`, 'info');
|
|
4945
|
-
const resetOptions = {
|
|
4946
|
-
...options,
|
|
4947
|
-
refreshCenterPrice: options.refreshCenterPrice !== false,
|
|
4948
|
-
};
|
|
4949
|
-
// Skip acquire if no lock exists (no fill processing to serialize against)
|
|
4950
|
-
// or if the caller already holds the lock (re-entrant). In the latter case
|
|
4951
|
-
// the lock's isReentrant() check prevents queueing and runs inline.
|
|
4952
|
-
// Otherwise, wait on the queue — this is the intended path for callers
|
|
4953
|
-
// outside the fill-processing context that need exclusive access.
|
|
4954
|
-
if (!this.manager._fillProcessingLock || this.manager._fillProcessingLock.isReentrant()) {
|
|
4955
|
-
return this._performGridResync(resetOptions);
|
|
4956
|
-
}
|
|
4957
|
-
return this.manager._fillProcessingLock.acquire(async () => this._performGridResync(resetOptions));
|
|
1672
|
+
return dexbot_maintenance_runtime_1.default.requestGridReset(this, reason, options);
|
|
4958
1673
|
}
|
|
4959
1674
|
_wireStructuralGridResyncRequest() {
|
|
4960
|
-
|
|
4961
|
-
return;
|
|
4962
|
-
this.manager.requestStructuralGridResync = async (reason = 'structural recovery', details = {}) => {
|
|
4963
|
-
if (this._shuttingDown) {
|
|
4964
|
-
return { skipped: true, reason: 'shutting down' };
|
|
4965
|
-
}
|
|
4966
|
-
if (this._structuralGridResyncRunning || this._structuralGridResyncTimer) {
|
|
4967
|
-
return { skipped: true, reason: 'structural grid resync already scheduled' };
|
|
4968
|
-
}
|
|
4969
|
-
const unmatchedCount = Array.isArray(details?.unmatchedChainOrders)
|
|
4970
|
-
? details.unmatchedChainOrders.length
|
|
4971
|
-
: 0;
|
|
4972
|
-
this._structuralGridResyncTimer = setTimeout(async () => {
|
|
4973
|
-
this._structuralGridResyncTimer = null;
|
|
4974
|
-
if (this._shuttingDown)
|
|
4975
|
-
return;
|
|
4976
|
-
this._structuralGridResyncRunning = true;
|
|
4977
|
-
try {
|
|
4978
|
-
// Try the lighter persisted-grid reload before full reset.
|
|
4979
|
-
const persistedResult = await this._recoverFromPersistedGrid();
|
|
4980
|
-
if (persistedResult.success) {
|
|
4981
|
-
if (this.manager?._recoveryState) {
|
|
4982
|
-
this.manager._recoveryState = { ...this.manager._recoveryState, attemptCount: 0, lastAttemptAt: 0, lastFailureAt: 0 };
|
|
4983
|
-
}
|
|
4984
|
-
return;
|
|
4985
|
-
}
|
|
4986
|
-
const suffix = unmatchedCount > 0 ? ` (${unmatchedCount} unmatched chain order(s))` : '';
|
|
4987
|
-
this._warn(`[RECOVERY] Running structural full grid resync for ${reason}${suffix}`);
|
|
4988
|
-
const resetResult = await this.requestGridReset('rms_structural_grid_resync', {
|
|
4989
|
-
refreshCenterPrice: true,
|
|
4990
|
-
});
|
|
4991
|
-
if (resetResult && this.manager?._recoveryState) {
|
|
4992
|
-
this.manager._recoveryState = { ...this.manager._recoveryState, attemptCount: 0, lastAttemptAt: 0, lastFailureAt: 0 };
|
|
4993
|
-
}
|
|
4994
|
-
}
|
|
4995
|
-
catch (err) {
|
|
4996
|
-
this._warn(`[RECOVERY] Structural full grid resync failed: ${err.message}`);
|
|
4997
|
-
}
|
|
4998
|
-
finally {
|
|
4999
|
-
this._structuralGridResyncRunning = false;
|
|
5000
|
-
if (this.manager?._recoveryState) {
|
|
5001
|
-
this.manager._recoveryState = { ...this.manager._recoveryState, structuralResyncRequested: false };
|
|
5002
|
-
}
|
|
5003
|
-
}
|
|
5004
|
-
}, 0);
|
|
5005
|
-
return { scheduled: true };
|
|
5006
|
-
};
|
|
1675
|
+
return dexbot_maintenance_runtime_1.default.wireStructuralGridResyncRequest(this);
|
|
5007
1676
|
}
|
|
5008
1677
|
/**
|
|
5009
1678
|
* Get current metrics for monitoring and debugging.
|
|
5010
1679
|
* @returns {Object} Metrics snapshot
|
|
5011
1680
|
*/
|
|
5012
1681
|
getMetrics() {
|
|
5013
|
-
|
|
5014
|
-
return {
|
|
5015
|
-
...this._metrics,
|
|
5016
|
-
queueDepth: this._incomingFillQueue.length,
|
|
5017
|
-
fillProcessingLockActive: this.manager?._fillProcessingLock?.isLocked() || false,
|
|
5018
|
-
divergenceLockActive: this.manager?._divergenceLock?.isLocked() || false,
|
|
5019
|
-
shadowLocksActive: this.manager?.shadowOrderIds?.size || 0,
|
|
5020
|
-
recoveryExhaustedAt: this.manager?._recoveryExhaustedAt || null,
|
|
5021
|
-
recentFillsTracked: this._recentlyProcessedFills.size
|
|
5022
|
-
};
|
|
1682
|
+
return dexbot_maintenance_runtime_1.default.getMetrics(this);
|
|
5023
1683
|
}
|
|
5024
1684
|
/**
|
|
5025
1685
|
* Execute grid maintenance checks in strict order with pipeline consensus.
|
|
@@ -5053,7 +1713,7 @@ class DEXBot {
|
|
|
5053
1713
|
* @private
|
|
5054
1714
|
*/
|
|
5055
1715
|
async _executeMaintenanceLogic(context) {
|
|
5056
|
-
return
|
|
1716
|
+
return dexbot_maintenance_runtime_1.default.executeMaintenanceLogic(this, context);
|
|
5057
1717
|
}
|
|
5058
1718
|
/**
|
|
5059
1719
|
* Cancel dust partial orders immediately — no maps, no timers, no delay.
|
|
@@ -5064,7 +1724,7 @@ class DEXBot {
|
|
|
5064
1724
|
* @private
|
|
5065
1725
|
*/
|
|
5066
1726
|
async _cancelDustOrders({ buy: buyDust = [], sell: sellDust = [] } = {}) {
|
|
5067
|
-
return
|
|
1727
|
+
return dexbot_maintenance_runtime_1.default.cancelDustOrders(this, { buy: buyDust, sell: sellDust });
|
|
5068
1728
|
}
|
|
5069
1729
|
/**
|
|
5070
1730
|
* Run a single dust health check cycle: inspect grid for partials below the
|
|
@@ -5074,41 +1734,7 @@ class DEXBot {
|
|
|
5074
1734
|
* @private
|
|
5075
1735
|
*/
|
|
5076
1736
|
async _runDustHealthCheck() {
|
|
5077
|
-
|
|
5078
|
-
return;
|
|
5079
|
-
try {
|
|
5080
|
-
const health = await this.manager.checkGridHealth(this.updateOrdersOnChainPlan?.bind(this));
|
|
5081
|
-
const buyDust = health.buyDustOrders || [];
|
|
5082
|
-
const sellDust = health.sellDustOrders || [];
|
|
5083
|
-
const totalDust = buyDust.length + sellDust.length;
|
|
5084
|
-
if (totalDust > 0) {
|
|
5085
|
-
this._log(`[DUST] Health check: ${totalDust} dust order(s) (buy=${buyDust.length}, sell=${sellDust.length})`);
|
|
5086
|
-
const lock = this.manager._fillProcessingLock;
|
|
5087
|
-
if (lock && typeof lock.acquire === 'function') {
|
|
5088
|
-
await lock.acquire(async () => {
|
|
5089
|
-
await this._cancelDustOrders({
|
|
5090
|
-
buy: health.buyDustOrders,
|
|
5091
|
-
sell: health.sellDustOrders,
|
|
5092
|
-
});
|
|
5093
|
-
}, { timeout: TIMING.DUST_CANCEL_TIMEOUT_MS });
|
|
5094
|
-
}
|
|
5095
|
-
else {
|
|
5096
|
-
this._warn('[DUST] Fill lock unavailable — cancelling dust without lock (potential race)');
|
|
5097
|
-
await this._cancelDustOrders({
|
|
5098
|
-
buy: health.buyDustOrders,
|
|
5099
|
-
sell: health.sellDustOrders,
|
|
5100
|
-
});
|
|
5101
|
-
}
|
|
5102
|
-
}
|
|
5103
|
-
}
|
|
5104
|
-
catch (err) {
|
|
5105
|
-
if (err?.message?.includes('Lock acquisition timeout')) {
|
|
5106
|
-
this._warn('[DUST] Lock busy, skipping dust cancel this cycle (retry in 5 min)');
|
|
5107
|
-
}
|
|
5108
|
-
else {
|
|
5109
|
-
this._warn(`[DUST] Health check error (retry in 5 min): ${err?.message || err}`);
|
|
5110
|
-
}
|
|
5111
|
-
}
|
|
1737
|
+
return dexbot_maintenance_runtime_1.default.runDustHealthCheck(this);
|
|
5112
1738
|
}
|
|
5113
1739
|
/**
|
|
5114
1740
|
* Set up the periodic dust health check interval.
|
|
@@ -5118,12 +1744,7 @@ class DEXBot {
|
|
|
5118
1744
|
* @private
|
|
5119
1745
|
*/
|
|
5120
1746
|
_setupDustHealthCheckInterval() {
|
|
5121
|
-
|
|
5122
|
-
this._runDustHealthCheck();
|
|
5123
|
-
}, TIMING.DUST_HEALTH_CHECK_INTERVAL_MS);
|
|
5124
|
-
if (typeof this._dustHealthCheckTimer?.unref === 'function') {
|
|
5125
|
-
this._dustHealthCheckTimer.unref();
|
|
5126
|
-
}
|
|
1747
|
+
return dexbot_maintenance_runtime_1.default.setupDustHealthCheckInterval(this);
|
|
5127
1748
|
}
|
|
5128
1749
|
/**
|
|
5129
1750
|
* Perform grid maintenance: fund thresholds, spread condition, grid health, divergence.
|
|
@@ -5150,7 +1771,7 @@ class DEXBot {
|
|
|
5150
1771
|
* @private
|
|
5151
1772
|
*/
|
|
5152
1773
|
async _runGridMaintenance(context = 'periodic', options = {}) {
|
|
5153
|
-
return
|
|
1774
|
+
return dexbot_maintenance_runtime_1.default.runGridMaintenance(this, context, options);
|
|
5154
1775
|
}
|
|
5155
1776
|
/**
|
|
5156
1777
|
* Gracefully shutdown the bot.
|
|
@@ -5207,7 +1828,7 @@ class DEXBot {
|
|
|
5207
1828
|
await this._creditRuntime.shutdown();
|
|
5208
1829
|
}
|
|
5209
1830
|
catch (err) {
|
|
5210
|
-
this._warn(`Failed to persist credit runtime state: ${err
|
|
1831
|
+
this._warn(`Failed to persist credit runtime state: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
5211
1832
|
}
|
|
5212
1833
|
}
|
|
5213
1834
|
if (this._triggerWatcher && typeof this._triggerWatcher.close === 'function') {
|
|
@@ -5215,7 +1836,7 @@ class DEXBot {
|
|
|
5215
1836
|
this._triggerWatcher.close();
|
|
5216
1837
|
}
|
|
5217
1838
|
catch (err) {
|
|
5218
|
-
this._warn(`Failed to close trigger watcher: ${err
|
|
1839
|
+
this._warn(`Failed to close trigger watcher: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
5219
1840
|
}
|
|
5220
1841
|
finally {
|
|
5221
1842
|
this._triggerWatcher = null;
|
|
@@ -5226,7 +1847,7 @@ class DEXBot {
|
|
|
5226
1847
|
await this._fillsUnsubscribe();
|
|
5227
1848
|
}
|
|
5228
1849
|
catch (err) {
|
|
5229
|
-
this._warn(`Failed to unsubscribe fill listener: ${err
|
|
1850
|
+
this._warn(`Failed to unsubscribe fill listener: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
5230
1851
|
}
|
|
5231
1852
|
finally {
|
|
5232
1853
|
this._fillsUnsubscribe = null;
|
|
@@ -5237,7 +1858,7 @@ class DEXBot {
|
|
|
5237
1858
|
this._reconnectUnregister();
|
|
5238
1859
|
}
|
|
5239
1860
|
catch (err) {
|
|
5240
|
-
this._warn(`Error unregistering reconnect callback: ${err
|
|
1861
|
+
this._warn(`Error unregistering reconnect callback: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
5241
1862
|
}
|
|
5242
1863
|
this._reconnectUnregister = null;
|
|
5243
1864
|
}
|
|
@@ -5245,13 +1866,13 @@ class DEXBot {
|
|
|
5245
1866
|
await this._stopOpenOrdersSyncLoop();
|
|
5246
1867
|
}
|
|
5247
1868
|
catch (err) {
|
|
5248
|
-
this._warn(`Error while stopping open-orders sync loop: ${err
|
|
1869
|
+
this._warn(`Error while stopping open-orders sync loop: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
5249
1870
|
}
|
|
5250
1871
|
try {
|
|
5251
1872
|
await this._releaseMarketAdapterRuntime('shutdown');
|
|
5252
1873
|
}
|
|
5253
1874
|
catch (err) {
|
|
5254
|
-
this._warn(`Error while releasing market adapter runtime: ${err
|
|
1875
|
+
this._warn(`Error while releasing market adapter runtime: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
5255
1876
|
}
|
|
5256
1877
|
// Wait for current fill processing to complete
|
|
5257
1878
|
try {
|
|
@@ -5289,7 +1910,7 @@ class DEXBot {
|
|
|
5289
1910
|
this._log('Final grid snapshot persisted');
|
|
5290
1911
|
}
|
|
5291
1912
|
catch (err) {
|
|
5292
|
-
this._warn(`Failed to persist final state: ${err
|
|
1913
|
+
this._warn(`Failed to persist final state: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
5293
1914
|
}
|
|
5294
1915
|
}
|
|
5295
1916
|
}).then(() => 'acquired'),
|
|
@@ -5328,12 +1949,12 @@ class DEXBot {
|
|
|
5328
1949
|
this._log('Final grid snapshot persisted (best-effort, lock not held)');
|
|
5329
1950
|
}
|
|
5330
1951
|
catch (err) {
|
|
5331
|
-
this._warn(`Failed to persist final state (best-effort): ${err
|
|
1952
|
+
this._warn(`Failed to persist final state (best-effort): ${(0, errors_1.getErrorMessage)(err)}`);
|
|
5332
1953
|
}
|
|
5333
1954
|
}
|
|
5334
1955
|
}
|
|
5335
1956
|
catch (err) {
|
|
5336
|
-
this._warn(`Best-effort flush during shutdown failed: ${err
|
|
1957
|
+
this._warn(`Best-effort flush during shutdown failed: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
5337
1958
|
}
|
|
5338
1959
|
}
|
|
5339
1960
|
// Always reset the fill-consumer watchdog on shutdown completion,
|
|
@@ -5343,14 +1964,14 @@ class DEXBot {
|
|
|
5343
1964
|
}
|
|
5344
1965
|
}
|
|
5345
1966
|
catch (err) {
|
|
5346
|
-
this._warn(`Error during shutdown lock acquisition: ${err
|
|
1967
|
+
this._warn(`Error during shutdown lock acquisition: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
5347
1968
|
}
|
|
5348
1969
|
// Release fund registry allocation
|
|
5349
1970
|
if (this.config?.preferredAccount) {
|
|
5350
1971
|
const botName = this.config.botKey;
|
|
5351
1972
|
if (botName) {
|
|
5352
1973
|
fundRegistry.releaseAllocation(this.config.preferredAccount, botName).catch((err) => {
|
|
5353
|
-
this._warn(`Failed to release fund allocation for ${botName}: ${err
|
|
1974
|
+
this._warn(`Failed to release fund allocation for ${botName}: ${(0, errors_1.getErrorMessage)(err)}`);
|
|
5354
1975
|
});
|
|
5355
1976
|
}
|
|
5356
1977
|
}
|
|
@@ -5367,7 +1988,7 @@ class DEXBot {
|
|
|
5367
1988
|
await this.manager?.logger?.flush();
|
|
5368
1989
|
}
|
|
5369
1990
|
}
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
1991
|
+
DEXBot.normalizeBotEntry = bot_settings_1.normalizeBotEntry;
|
|
1992
|
+
exports.default = DEXBot;
|
|
1993
|
+
module.exports = DEXBot;
|
|
5373
1994
|
//# sourceMappingURL=dexbot_class.js.map
|