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.
Files changed (738) hide show
  1. package/dist/analysis/chart_utils.d.ts +11 -9
  2. package/dist/analysis/chart_utils.d.ts.map +1 -1
  3. package/dist/analysis/chart_utils.js +21 -16
  4. package/dist/analysis/chart_utils.js.map +1 -1
  5. package/dist/analysis/math_utils.d.ts +4 -13
  6. package/dist/analysis/math_utils.d.ts.map +1 -1
  7. package/dist/analysis/math_utils.js +16 -18
  8. package/dist/analysis/math_utils.js.map +1 -1
  9. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts +3 -7
  10. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
  11. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +38 -34
  12. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
  13. package/dist/analysis/trend_detection/hurst_analyzer.d.ts +1 -4
  14. package/dist/analysis/trend_detection/hurst_analyzer.d.ts.map +1 -1
  15. package/dist/analysis/trend_detection/hurst_analyzer.js +10 -7
  16. package/dist/analysis/trend_detection/hurst_analyzer.js.map +1 -1
  17. package/dist/analysis/trend_detection/kalman_trend_analyzer.d.ts +6 -6
  18. package/dist/analysis/trend_detection/kalman_trend_analyzer.d.ts.map +1 -1
  19. package/dist/analysis/trend_detection/kalman_trend_analyzer.js +23 -15
  20. package/dist/analysis/trend_detection/kalman_trend_analyzer.js.map +1 -1
  21. package/dist/analysis/trend_detection/kalman_velocity_smoothing.d.ts +12 -13
  22. package/dist/analysis/trend_detection/kalman_velocity_smoothing.d.ts.map +1 -1
  23. package/dist/analysis/trend_detection/kalman_velocity_smoothing.js +16 -16
  24. package/dist/analysis/trend_detection/kalman_velocity_smoothing.js.map +1 -1
  25. package/dist/analysis/trend_detection/permutation_entropy_analyzer.d.ts +1 -4
  26. package/dist/analysis/trend_detection/permutation_entropy_analyzer.d.ts.map +1 -1
  27. package/dist/analysis/trend_detection/permutation_entropy_analyzer.js +10 -7
  28. package/dist/analysis/trend_detection/permutation_entropy_analyzer.js.map +1 -1
  29. package/dist/bot.d.ts.map +1 -1
  30. package/dist/bot.js +6 -13
  31. package/dist/bot.js.map +1 -1
  32. package/dist/credential-daemon.d.ts.map +1 -1
  33. package/dist/credential-daemon.js +74 -35
  34. package/dist/credential-daemon.js.map +1 -1
  35. package/dist/dexbot.d.ts.map +1 -1
  36. package/dist/dexbot.js +16 -15
  37. package/dist/dexbot.js.map +1 -1
  38. package/dist/market_adapter/ama_signal_runner.d.ts +1 -1
  39. package/dist/market_adapter/ama_signal_runner.d.ts.map +1 -1
  40. package/dist/market_adapter/ama_signal_runner.js +8 -20
  41. package/dist/market_adapter/ama_signal_runner.js.map +1 -1
  42. package/dist/market_adapter/candle_utils.d.ts +2 -10
  43. package/dist/market_adapter/candle_utils.d.ts.map +1 -1
  44. package/dist/market_adapter/candle_utils.js +7 -8
  45. package/dist/market_adapter/candle_utils.js.map +1 -1
  46. package/dist/market_adapter/core/asymmetric_bounds.d.ts +12 -7
  47. package/dist/market_adapter/core/asymmetric_bounds.d.ts.map +1 -1
  48. package/dist/market_adapter/core/asymmetric_bounds.js +4 -5
  49. package/dist/market_adapter/core/asymmetric_bounds.js.map +1 -1
  50. package/dist/market_adapter/core/config_normalizers.d.ts +4 -9
  51. package/dist/market_adapter/core/config_normalizers.d.ts.map +1 -1
  52. package/dist/market_adapter/core/config_normalizers.js +9 -9
  53. package/dist/market_adapter/core/config_normalizers.js.map +1 -1
  54. package/dist/market_adapter/core/kibana_candles.d.ts +16 -20
  55. package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
  56. package/dist/market_adapter/core/kibana_candles.js +16 -15
  57. package/dist/market_adapter/core/kibana_candles.js.map +1 -1
  58. package/dist/market_adapter/core/kibana_client.d.ts +8 -11
  59. package/dist/market_adapter/core/kibana_client.d.ts.map +1 -1
  60. package/dist/market_adapter/core/kibana_client.js +13 -11
  61. package/dist/market_adapter/core/kibana_client.js.map +1 -1
  62. package/dist/market_adapter/core/kibana_market_candles.d.ts +5 -12
  63. package/dist/market_adapter/core/kibana_market_candles.d.ts.map +1 -1
  64. package/dist/market_adapter/core/kibana_market_candles.js +12 -21
  65. package/dist/market_adapter/core/kibana_market_candles.js.map +1 -1
  66. package/dist/market_adapter/core/market_adapter_service.d.ts +75 -53
  67. package/dist/market_adapter/core/market_adapter_service.d.ts.map +1 -1
  68. package/dist/market_adapter/core/market_adapter_service.js +102 -98
  69. package/dist/market_adapter/core/market_adapter_service.js.map +1 -1
  70. package/dist/market_adapter/core/strategies/ama.d.ts +21 -1
  71. package/dist/market_adapter/core/strategies/ama.d.ts.map +1 -1
  72. package/dist/market_adapter/core/strategies/ama.js +10 -10
  73. package/dist/market_adapter/core/strategies/ama.js.map +1 -1
  74. package/dist/market_adapter/core/strategies/ama_slope_model.d.ts +7 -11
  75. package/dist/market_adapter/core/strategies/ama_slope_model.d.ts.map +1 -1
  76. package/dist/market_adapter/core/strategies/ama_slope_model.js +23 -30
  77. package/dist/market_adapter/core/strategies/ama_slope_model.js.map +1 -1
  78. package/dist/market_adapter/core/strategies/atr/calculator.d.ts +5 -4
  79. package/dist/market_adapter/core/strategies/atr/calculator.d.ts.map +1 -1
  80. package/dist/market_adapter/core/strategies/atr/calculator.js +5 -3
  81. package/dist/market_adapter/core/strategies/atr/calculator.js.map +1 -1
  82. package/dist/market_adapter/core/strategies/collateral_manager.d.ts +4 -7
  83. package/dist/market_adapter/core/strategies/collateral_manager.d.ts.map +1 -1
  84. package/dist/market_adapter/core/strategies/collateral_manager.js +5 -4
  85. package/dist/market_adapter/core/strategies/collateral_manager.js.map +1 -1
  86. package/dist/market_adapter/core/strategies/regime_gate.d.ts +11 -19
  87. package/dist/market_adapter/core/strategies/regime_gate.d.ts.map +1 -1
  88. package/dist/market_adapter/core/strategies/regime_gate.js +22 -22
  89. package/dist/market_adapter/core/strategies/regime_gate.js.map +1 -1
  90. package/dist/market_adapter/index.d.ts +24 -2
  91. package/dist/market_adapter/index.d.ts.map +1 -1
  92. package/dist/market_adapter/index.js +82 -56
  93. package/dist/market_adapter/index.js.map +1 -1
  94. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +26 -37
  95. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js.map +1 -1
  96. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +4 -12
  97. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  98. package/dist/market_adapter/inputs/fetch_lp_data.js +100 -66
  99. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  100. package/dist/market_adapter/inputs/kibana_source.d.ts +4 -9
  101. package/dist/market_adapter/inputs/kibana_source.d.ts.map +1 -1
  102. package/dist/market_adapter/inputs/kibana_source.js +17 -18
  103. package/dist/market_adapter/inputs/kibana_source.js.map +1 -1
  104. package/dist/market_adapter/interval_utils.d.ts +1 -4
  105. package/dist/market_adapter/interval_utils.d.ts.map +1 -1
  106. package/dist/market_adapter/interval_utils.js +2 -3
  107. package/dist/market_adapter/interval_utils.js.map +1 -1
  108. package/dist/market_adapter/log_format.d.ts +1 -9
  109. package/dist/market_adapter/log_format.d.ts.map +1 -1
  110. package/dist/market_adapter/log_format.js +7 -8
  111. package/dist/market_adapter/log_format.js.map +1 -1
  112. package/dist/market_adapter/lp_chart_core.d.ts +1 -4
  113. package/dist/market_adapter/lp_chart_core.d.ts.map +1 -1
  114. package/dist/market_adapter/lp_chart_core.js +22 -20
  115. package/dist/market_adapter/lp_chart_core.js.map +1 -1
  116. package/dist/market_adapter/lp_chart_runner.d.ts +13 -24
  117. package/dist/market_adapter/lp_chart_runner.d.ts.map +1 -1
  118. package/dist/market_adapter/lp_chart_runner.js +58 -56
  119. package/dist/market_adapter/lp_chart_runner.js.map +1 -1
  120. package/dist/market_adapter/lp_chart_strategy_loader.d.ts +4 -17
  121. package/dist/market_adapter/lp_chart_strategy_loader.d.ts.map +1 -1
  122. package/dist/market_adapter/lp_chart_strategy_loader.js +36 -36
  123. package/dist/market_adapter/lp_chart_strategy_loader.js.map +1 -1
  124. package/dist/market_adapter/market_adapter.d.ts +51 -72
  125. package/dist/market_adapter/market_adapter.d.ts.map +1 -1
  126. package/dist/market_adapter/market_adapter.js +227 -192
  127. package/dist/market_adapter/market_adapter.js.map +1 -1
  128. package/dist/market_adapter/merge_lp_data.js +17 -16
  129. package/dist/market_adapter/merge_lp_data.js.map +1 -1
  130. package/dist/market_adapter/test_helpers.d.ts +9 -13
  131. package/dist/market_adapter/test_helpers.d.ts.map +1 -1
  132. package/dist/market_adapter/test_helpers.js +14 -14
  133. package/dist/market_adapter/test_helpers.js.map +1 -1
  134. package/dist/market_adapter/utils/adapter_client.d.ts +6 -14
  135. package/dist/market_adapter/utils/adapter_client.d.ts.map +1 -1
  136. package/dist/market_adapter/utils/adapter_client.js +16 -15
  137. package/dist/market_adapter/utils/adapter_client.js.map +1 -1
  138. package/dist/market_adapter/utils/atomic_write.d.ts +2 -5
  139. package/dist/market_adapter/utils/atomic_write.d.ts.map +1 -1
  140. package/dist/market_adapter/utils/atomic_write.js +5 -5
  141. package/dist/market_adapter/utils/atomic_write.js.map +1 -1
  142. package/dist/market_adapter/utils/chain.d.ts +19 -21
  143. package/dist/market_adapter/utils/chain.d.ts.map +1 -1
  144. package/dist/market_adapter/utils/chain.js +15 -16
  145. package/dist/market_adapter/utils/chain.js.map +1 -1
  146. package/dist/market_adapter/utils/data_discovery.d.ts +1 -4
  147. package/dist/market_adapter/utils/data_discovery.d.ts.map +1 -1
  148. package/dist/market_adapter/utils/data_discovery.js +10 -10
  149. package/dist/market_adapter/utils/data_discovery.js.map +1 -1
  150. package/dist/market_adapter/utils/dynamic_grid_snapshot.d.ts +1 -6
  151. package/dist/market_adapter/utils/dynamic_grid_snapshot.d.ts.map +1 -1
  152. package/dist/market_adapter/utils/dynamic_grid_snapshot.js +14 -14
  153. package/dist/market_adapter/utils/dynamic_grid_snapshot.js.map +1 -1
  154. package/dist/market_adapter/utils/file_lock.d.ts +1 -7
  155. package/dist/market_adapter/utils/file_lock.d.ts.map +1 -1
  156. package/dist/market_adapter/utils/file_lock.js +21 -25
  157. package/dist/market_adapter/utils/file_lock.js.map +1 -1
  158. package/dist/market_adapter/utils/native_history.d.ts +12 -12
  159. package/dist/market_adapter/utils/native_history.d.ts.map +1 -1
  160. package/dist/market_adapter/utils/native_history.js +15 -15
  161. package/dist/market_adapter/utils/native_history.js.map +1 -1
  162. package/dist/market_adapter/utils/paths.d.ts +2 -4
  163. package/dist/market_adapter/utils/paths.d.ts.map +1 -1
  164. package/dist/market_adapter/utils/paths.js +6 -5
  165. package/dist/market_adapter/utils/paths.js.map +1 -1
  166. package/dist/modules/account_bots.d.ts +7 -8
  167. package/dist/modules/account_bots.d.ts.map +1 -1
  168. package/dist/modules/account_bots.js +92 -114
  169. package/dist/modules/account_bots.js.map +1 -1
  170. package/dist/modules/account_orders.d.ts +5 -9
  171. package/dist/modules/account_orders.d.ts.map +1 -1
  172. package/dist/modules/account_orders.js +69 -32
  173. package/dist/modules/account_orders.js.map +1 -1
  174. package/dist/modules/authority_resolver.d.ts +32 -1
  175. package/dist/modules/authority_resolver.d.ts.map +1 -1
  176. package/dist/modules/authority_resolver.js +18 -12
  177. package/dist/modules/authority_resolver.js.map +1 -1
  178. package/dist/modules/bitshares-native/chain_client.d.ts +3 -8
  179. package/dist/modules/bitshares-native/chain_client.d.ts.map +1 -1
  180. package/dist/modules/bitshares-native/chain_client.js +21 -16
  181. package/dist/modules/bitshares-native/chain_client.js.map +1 -1
  182. package/dist/modules/bitshares-native/crypto/ecc.browser.d.ts +21 -29
  183. package/dist/modules/bitshares-native/crypto/ecc.browser.d.ts.map +1 -1
  184. package/dist/modules/bitshares-native/crypto/ecc.browser.js +72 -38
  185. package/dist/modules/bitshares-native/crypto/ecc.browser.js.map +1 -1
  186. package/dist/modules/bitshares-native/crypto/ecc.d.ts +9 -36
  187. package/dist/modules/bitshares-native/crypto/ecc.d.ts.map +1 -1
  188. package/dist/modules/bitshares-native/crypto/ecc.js +40 -83
  189. package/dist/modules/bitshares-native/crypto/ecc.js.map +1 -1
  190. package/dist/modules/bitshares-native/crypto/ecc_selector.d.ts +14 -1
  191. package/dist/modules/bitshares-native/crypto/ecc_selector.d.ts.map +1 -1
  192. package/dist/modules/bitshares-native/crypto/ecc_selector.js +5 -3
  193. package/dist/modules/bitshares-native/crypto/ecc_selector.js.map +1 -1
  194. package/dist/modules/bitshares-native/index.d.ts +10 -2
  195. package/dist/modules/bitshares-native/index.d.ts.map +1 -1
  196. package/dist/modules/bitshares-native/index.js +68 -31
  197. package/dist/modules/bitshares-native/index.js.map +1 -1
  198. package/dist/modules/bitshares-native/lru_cache.d.ts +1 -4
  199. package/dist/modules/bitshares-native/lru_cache.d.ts.map +1 -1
  200. package/dist/modules/bitshares-native/lru_cache.js +6 -3
  201. package/dist/modules/bitshares-native/lru_cache.js.map +1 -1
  202. package/dist/modules/bitshares-native/resolvers.d.ts +3 -6
  203. package/dist/modules/bitshares-native/resolvers.d.ts.map +1 -1
  204. package/dist/modules/bitshares-native/resolvers.js +11 -9
  205. package/dist/modules/bitshares-native/resolvers.js.map +1 -1
  206. package/dist/modules/bitshares-native/serial/chain_constants.d.ts +53 -55
  207. package/dist/modules/bitshares-native/serial/chain_constants.d.ts.map +1 -1
  208. package/dist/modules/bitshares-native/serial/chain_constants.js +28 -27
  209. package/dist/modules/bitshares-native/serial/chain_constants.js.map +1 -1
  210. package/dist/modules/bitshares-native/serial/index.d.ts +5 -9
  211. package/dist/modules/bitshares-native/serial/index.d.ts.map +1 -1
  212. package/dist/modules/bitshares-native/serial/index.js +46 -12
  213. package/dist/modules/bitshares-native/serial/index.js.map +1 -1
  214. package/dist/modules/bitshares-native/serial/operations.d.ts +102 -100
  215. package/dist/modules/bitshares-native/serial/operations.d.ts.map +1 -1
  216. package/dist/modules/bitshares-native/serial/operations.js +473 -471
  217. package/dist/modules/bitshares-native/serial/operations.js.map +1 -1
  218. package/dist/modules/bitshares-native/serial/serializer.d.ts +2 -7
  219. package/dist/modules/bitshares-native/serial/serializer.d.ts.map +1 -1
  220. package/dist/modules/bitshares-native/serial/serializer.js +8 -3
  221. package/dist/modules/bitshares-native/serial/serializer.js.map +1 -1
  222. package/dist/modules/bitshares-native/serial/types.d.ts +26 -41
  223. package/dist/modules/bitshares-native/serial/types.d.ts.map +1 -1
  224. package/dist/modules/bitshares-native/serial/types.js +76 -42
  225. package/dist/modules/bitshares-native/serial/types.js.map +1 -1
  226. package/dist/modules/bitshares-native/signing_client.d.ts +1 -5
  227. package/dist/modules/bitshares-native/signing_client.d.ts.map +1 -1
  228. package/dist/modules/bitshares-native/signing_client.js +49 -11
  229. package/dist/modules/bitshares-native/signing_client.js.map +1 -1
  230. package/dist/modules/bitshares-native/subscriptions.d.ts +1 -4
  231. package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
  232. package/dist/modules/bitshares-native/subscriptions.js +47 -84
  233. package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
  234. package/dist/modules/bitshares-native/transport.d.ts +1 -8
  235. package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
  236. package/dist/modules/bitshares-native/transport.js +22 -15
  237. package/dist/modules/bitshares-native/transport.js.map +1 -1
  238. package/dist/modules/bitshares-native/tx/builder.d.ts +3 -8
  239. package/dist/modules/bitshares-native/tx/builder.d.ts.map +1 -1
  240. package/dist/modules/bitshares-native/tx/builder.js +61 -23
  241. package/dist/modules/bitshares-native/tx/builder.js.map +1 -1
  242. package/dist/modules/bitshares-native/tx/tx_cache.d.ts +1 -8
  243. package/dist/modules/bitshares-native/tx/tx_cache.d.ts.map +1 -1
  244. package/dist/modules/bitshares-native/tx/tx_cache.js +21 -17
  245. package/dist/modules/bitshares-native/tx/tx_cache.js.map +1 -1
  246. package/dist/modules/bitshares_client.d.ts +12 -23
  247. package/dist/modules/bitshares_client.d.ts.map +1 -1
  248. package/dist/modules/bitshares_client.js +109 -56
  249. package/dist/modules/bitshares_client.js.map +1 -1
  250. package/dist/modules/bot_settings.d.ts +1 -13
  251. package/dist/modules/bot_settings.d.ts.map +1 -1
  252. package/dist/modules/bot_settings.js +34 -34
  253. package/dist/modules/bot_settings.js.map +1 -1
  254. package/dist/modules/bots_file_lock.d.ts +3 -9
  255. package/dist/modules/bots_file_lock.d.ts.map +1 -1
  256. package/dist/modules/bots_file_lock.js +8 -9
  257. package/dist/modules/bots_file_lock.js.map +1 -1
  258. package/dist/modules/chain_keys.d.ts +7 -39
  259. package/dist/modules/chain_keys.d.ts.map +1 -1
  260. package/dist/modules/chain_keys.js +126 -90
  261. package/dist/modules/chain_keys.js.map +1 -1
  262. package/dist/modules/chain_orders.d.ts +46 -52
  263. package/dist/modules/chain_orders.d.ts.map +1 -1
  264. package/dist/modules/chain_orders.js +152 -104
  265. package/dist/modules/chain_orders.js.map +1 -1
  266. package/dist/modules/config.d.ts +50 -57
  267. package/dist/modules/config.d.ts.map +1 -1
  268. package/dist/modules/config.js +28 -24
  269. package/dist/modules/config.js.map +1 -1
  270. package/dist/modules/constants.d.ts +492 -492
  271. package/dist/modules/constants.d.ts.map +1 -1
  272. package/dist/modules/constants.js +79 -39
  273. package/dist/modules/constants.js.map +1 -1
  274. package/dist/modules/cr_planner.d.ts +1 -13
  275. package/dist/modules/cr_planner.d.ts.map +1 -1
  276. package/dist/modules/cr_planner.js +28 -28
  277. package/dist/modules/cr_planner.js.map +1 -1
  278. package/dist/modules/credential_policy.d.ts +18 -33
  279. package/dist/modules/credential_policy.d.ts.map +1 -1
  280. package/dist/modules/credential_policy.js +114 -87
  281. package/dist/modules/credential_policy.js.map +1 -1
  282. package/dist/modules/credential_runtime.d.ts +11 -21
  283. package/dist/modules/credential_runtime.d.ts.map +1 -1
  284. package/dist/modules/credential_runtime.js +40 -40
  285. package/dist/modules/credential_runtime.js.map +1 -1
  286. package/dist/modules/credential_session_cache.d.ts +8 -8
  287. package/dist/modules/credential_session_cache.d.ts.map +1 -1
  288. package/dist/modules/credential_session_cache.js +41 -8
  289. package/dist/modules/credential_session_cache.js.map +1 -1
  290. package/dist/modules/credit_runtime.d.ts +51 -316
  291. package/dist/modules/credit_runtime.d.ts.map +1 -1
  292. package/dist/modules/credit_runtime.js +257 -183
  293. package/dist/modules/credit_runtime.js.map +1 -1
  294. package/dist/modules/crypto/browser_provider.js +6 -6
  295. package/dist/modules/crypto/browser_provider.js.map +1 -1
  296. package/dist/modules/crypto/index.d.ts.map +1 -1
  297. package/dist/modules/crypto/index.js +2 -0
  298. package/dist/modules/crypto/index.js.map +1 -1
  299. package/dist/modules/crypto/sync.js +3 -3
  300. package/dist/modules/crypto/sync.js.map +1 -1
  301. package/dist/modules/dexbot_class.d.ts +124 -73
  302. package/dist/modules/dexbot_class.d.ts.map +1 -1
  303. package/dist/modules/dexbot_class.js +198 -3577
  304. package/dist/modules/dexbot_class.js.map +1 -1
  305. package/dist/modules/dexbot_cow_runtime.d.ts +322 -0
  306. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -0
  307. package/dist/modules/dexbot_cow_runtime.js +1836 -0
  308. package/dist/modules/dexbot_cow_runtime.js.map +1 -0
  309. package/dist/modules/dexbot_credential_client.d.ts +5 -11
  310. package/dist/modules/dexbot_credential_client.d.ts.map +1 -1
  311. package/dist/modules/dexbot_credential_client.js +32 -30
  312. package/dist/modules/dexbot_credential_client.js.map +1 -1
  313. package/dist/modules/dexbot_fill_runtime.d.ts +47 -15
  314. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  315. package/dist/modules/dexbot_fill_runtime.js +565 -22
  316. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  317. package/dist/modules/dexbot_maintenance_runtime.d.ts +79 -10
  318. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  319. package/dist/modules/dexbot_maintenance_runtime.js +294 -55
  320. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  321. package/dist/modules/dexbot_startup_runtime.d.ts +26 -0
  322. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -0
  323. package/dist/modules/dexbot_startup_runtime.js +575 -0
  324. package/dist/modules/dexbot_startup_runtime.js.map +1 -0
  325. package/dist/modules/dexbot_state_recovery.d.ts +115 -0
  326. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -0
  327. package/dist/modules/dexbot_state_recovery.js +431 -0
  328. package/dist/modules/dexbot_state_recovery.js.map +1 -0
  329. package/dist/modules/fund_registry.d.ts +1 -13
  330. package/dist/modules/fund_registry.d.ts.map +1 -1
  331. package/dist/modules/fund_registry.js +23 -22
  332. package/dist/modules/fund_registry.js.map +1 -1
  333. package/dist/modules/general_settings.d.ts +3 -7
  334. package/dist/modules/general_settings.d.ts.map +1 -1
  335. package/dist/modules/general_settings.js +15 -15
  336. package/dist/modules/general_settings.js.map +1 -1
  337. package/dist/modules/graceful_shutdown.d.ts +1 -6
  338. package/dist/modules/graceful_shutdown.d.ts.map +1 -1
  339. package/dist/modules/graceful_shutdown.js +19 -16
  340. package/dist/modules/graceful_shutdown.js.map +1 -1
  341. package/dist/modules/key_store.d.ts.map +1 -1
  342. package/dist/modules/key_store.js +53 -20
  343. package/dist/modules/key_store.js.map +1 -1
  344. package/dist/modules/launcher/bot_supervisor.d.ts +23 -66
  345. package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
  346. package/dist/modules/launcher/bot_supervisor.js +115 -107
  347. package/dist/modules/launcher/bot_supervisor.js.map +1 -1
  348. package/dist/modules/launcher/child_env.d.ts +2 -5
  349. package/dist/modules/launcher/child_env.d.ts.map +1 -1
  350. package/dist/modules/launcher/child_env.js +4 -4
  351. package/dist/modules/launcher/child_env.js.map +1 -1
  352. package/dist/modules/launcher/credential_bootstrap.d.ts +4 -8
  353. package/dist/modules/launcher/credential_bootstrap.d.ts.map +1 -1
  354. package/dist/modules/launcher/credential_bootstrap.js +30 -26
  355. package/dist/modules/launcher/credential_bootstrap.js.map +1 -1
  356. package/dist/modules/launcher/credential_daemon.d.ts +8 -15
  357. package/dist/modules/launcher/credential_daemon.d.ts.map +1 -1
  358. package/dist/modules/launcher/credential_daemon.js +72 -41
  359. package/dist/modules/launcher/credential_daemon.js.map +1 -1
  360. package/dist/modules/launcher/credential_secret.d.ts +1 -4
  361. package/dist/modules/launcher/credential_secret.d.ts.map +1 -1
  362. package/dist/modules/launcher/credential_secret.js +36 -4
  363. package/dist/modules/launcher/credential_secret.js.map +1 -1
  364. package/dist/modules/launcher/foreign_cred_daemon.d.ts +32 -1
  365. package/dist/modules/launcher/foreign_cred_daemon.d.ts.map +1 -1
  366. package/dist/modules/launcher/foreign_cred_daemon.js +25 -28
  367. package/dist/modules/launcher/foreign_cred_daemon.js.map +1 -1
  368. package/dist/modules/launcher/headless_password.d.ts +1 -4
  369. package/dist/modules/launcher/headless_password.d.ts.map +1 -1
  370. package/dist/modules/launcher/headless_password.js +11 -11
  371. package/dist/modules/launcher/headless_password.js.map +1 -1
  372. package/dist/modules/launcher/launch_modes.d.ts +15 -14
  373. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  374. package/dist/modules/launcher/launch_modes.js +8 -8
  375. package/dist/modules/launcher/launch_modes.js.map +1 -1
  376. package/dist/modules/launcher/market_adapter_runtime.d.ts +10 -19
  377. package/dist/modules/launcher/market_adapter_runtime.d.ts.map +1 -1
  378. package/dist/modules/launcher/market_adapter_runtime.js +33 -34
  379. package/dist/modules/launcher/market_adapter_runtime.js.map +1 -1
  380. package/dist/modules/launcher/market_adapter_watchdog.d.ts +10 -17
  381. package/dist/modules/launcher/market_adapter_watchdog.d.ts.map +1 -1
  382. package/dist/modules/launcher/market_adapter_watchdog.js +42 -39
  383. package/dist/modules/launcher/market_adapter_watchdog.js.map +1 -1
  384. package/dist/modules/launcher/monolithic_runtime.d.ts +28 -57
  385. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  386. package/dist/modules/launcher/monolithic_runtime.js +135 -108
  387. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  388. package/dist/modules/launcher/runtime_entry.d.ts +5 -10
  389. package/dist/modules/launcher/runtime_entry.d.ts.map +1 -1
  390. package/dist/modules/launcher/runtime_entry.js +14 -14
  391. package/dist/modules/launcher/runtime_entry.js.map +1 -1
  392. package/dist/modules/launcher/status_reporting.d.ts +17 -37
  393. package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
  394. package/dist/modules/launcher/status_reporting.js +35 -34
  395. package/dist/modules/launcher/status_reporting.js.map +1 -1
  396. package/dist/modules/launcher/supervisor_control.d.ts +1 -4
  397. package/dist/modules/launcher/supervisor_control.d.ts.map +1 -1
  398. package/dist/modules/launcher/supervisor_control.js +9 -4
  399. package/dist/modules/launcher/supervisor_control.js.map +1 -1
  400. package/dist/modules/logger.d.ts +3 -1
  401. package/dist/modules/logger.d.ts.map +1 -1
  402. package/dist/modules/logger.js +38 -28
  403. package/dist/modules/logger.js.map +1 -1
  404. package/dist/modules/market_adapter_whitelist.d.ts +2 -10
  405. package/dist/modules/market_adapter_whitelist.d.ts.map +1 -1
  406. package/dist/modules/market_adapter_whitelist.js +18 -16
  407. package/dist/modules/market_adapter_whitelist.js.map +1 -1
  408. package/dist/modules/node_health_cache.d.ts +2 -12
  409. package/dist/modules/node_health_cache.d.ts.map +1 -1
  410. package/dist/modules/node_health_cache.js +24 -24
  411. package/dist/modules/node_health_cache.js.map +1 -1
  412. package/dist/modules/node_manager.d.ts +1 -1
  413. package/dist/modules/node_manager.d.ts.map +1 -1
  414. package/dist/modules/node_manager.js +44 -38
  415. package/dist/modules/node_manager.js.map +1 -1
  416. package/dist/modules/order/accounting.d.ts +12 -12
  417. package/dist/modules/order/accounting.d.ts.map +1 -1
  418. package/dist/modules/order/accounting.js +111 -76
  419. package/dist/modules/order/accounting.js.map +1 -1
  420. package/dist/modules/order/async_lock.d.ts +1 -2
  421. package/dist/modules/order/async_lock.d.ts.map +1 -1
  422. package/dist/modules/order/async_lock.js +4 -4
  423. package/dist/modules/order/async_lock.js.map +1 -1
  424. package/dist/modules/order/export.d.ts +8 -14
  425. package/dist/modules/order/export.d.ts.map +1 -1
  426. package/dist/modules/order/export.js +77 -42
  427. package/dist/modules/order/export.js.map +1 -1
  428. package/dist/modules/order/format.d.ts +17 -22
  429. package/dist/modules/order/format.d.ts.map +1 -1
  430. package/dist/modules/order/format.js +25 -31
  431. package/dist/modules/order/format.js.map +1 -1
  432. package/dist/modules/order/grid.d.ts +18 -104
  433. package/dist/modules/order/grid.d.ts.map +1 -1
  434. package/dist/modules/order/grid.js +246 -188
  435. package/dist/modules/order/grid.js.map +1 -1
  436. package/dist/modules/order/grid_reconcile.d.ts +8 -8
  437. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  438. package/dist/modules/order/grid_reconcile.js +24 -24
  439. package/dist/modules/order/grid_reconcile.js.map +1 -1
  440. package/dist/modules/order/grid_reconcile_internal.d.ts +224 -1
  441. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  442. package/dist/modules/order/grid_reconcile_internal.js +145 -72
  443. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  444. package/dist/modules/order/index.d.ts +1 -1
  445. package/dist/modules/order/index.d.ts.map +1 -1
  446. package/dist/modules/order/index.js +42 -7
  447. package/dist/modules/order/index.js.map +1 -1
  448. package/dist/modules/order/logger.d.ts +6 -16
  449. package/dist/modules/order/logger.d.ts.map +1 -1
  450. package/dist/modules/order/logger.js +76 -53
  451. package/dist/modules/order/logger.js.map +1 -1
  452. package/dist/modules/order/logger_state.d.ts +4 -4
  453. package/dist/modules/order/logger_state.d.ts.map +1 -1
  454. package/dist/modules/order/logger_state.js +3 -1
  455. package/dist/modules/order/logger_state.js.map +1 -1
  456. package/dist/modules/order/manager.d.ts +160 -37
  457. package/dist/modules/order/manager.d.ts.map +1 -1
  458. package/dist/modules/order/manager.js +166 -128
  459. package/dist/modules/order/manager.js.map +1 -1
  460. package/dist/modules/order/processed_fill_store.d.ts +6 -10
  461. package/dist/modules/order/processed_fill_store.d.ts.map +1 -1
  462. package/dist/modules/order/processed_fill_store.js +10 -9
  463. package/dist/modules/order/processed_fill_store.js.map +1 -1
  464. package/dist/modules/order/strategy.d.ts +3 -3
  465. package/dist/modules/order/strategy.d.ts.map +1 -1
  466. package/dist/modules/order/strategy.js +29 -27
  467. package/dist/modules/order/strategy.js.map +1 -1
  468. package/dist/modules/order/sync_engine.d.ts +9 -19
  469. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  470. package/dist/modules/order/sync_engine.js +176 -126
  471. package/dist/modules/order/sync_engine.js.map +1 -1
  472. package/dist/modules/order/utils/math.d.ts +55 -85
  473. package/dist/modules/order/utils/math.d.ts.map +1 -1
  474. package/dist/modules/order/utils/math.js +155 -88
  475. package/dist/modules/order/utils/math.js.map +1 -1
  476. package/dist/modules/order/utils/order.d.ts +27 -72
  477. package/dist/modules/order/utils/order.d.ts.map +1 -1
  478. package/dist/modules/order/utils/order.js +144 -105
  479. package/dist/modules/order/utils/order.js.map +1 -1
  480. package/dist/modules/order/utils/system.d.ts +74 -44
  481. package/dist/modules/order/utils/system.d.ts.map +1 -1
  482. package/dist/modules/order/utils/system.js +188 -149
  483. package/dist/modules/order/utils/system.js.map +1 -1
  484. package/dist/modules/order/utils/validate.d.ts +21 -43
  485. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  486. package/dist/modules/order/utils/validate.js +140 -110
  487. package/dist/modules/order/utils/validate.js.map +1 -1
  488. package/dist/modules/order/working_grid.d.ts +1 -4
  489. package/dist/modules/order/working_grid.d.ts.map +1 -1
  490. package/dist/modules/order/working_grid.js +9 -7
  491. package/dist/modules/order/working_grid.js.map +1 -1
  492. package/dist/modules/path_api.js +2 -2
  493. package/dist/modules/path_api.js.map +1 -1
  494. package/dist/modules/paths.d.ts +51 -55
  495. package/dist/modules/paths.d.ts.map +1 -1
  496. package/dist/modules/paths.js +72 -52
  497. package/dist/modules/paths.js.map +1 -1
  498. package/dist/modules/process_discovery.d.ts +1 -1
  499. package/dist/modules/process_discovery.d.ts.map +1 -1
  500. package/dist/modules/process_discovery.js +7 -7
  501. package/dist/modules/process_discovery.js.map +1 -1
  502. package/dist/modules/runtime.d.ts +1 -1
  503. package/dist/modules/runtime.d.ts.map +1 -1
  504. package/dist/modules/runtime.js +4 -4
  505. package/dist/modules/runtime.js.map +1 -1
  506. package/dist/modules/runtime_settings.d.ts +1 -0
  507. package/dist/modules/runtime_settings.d.ts.map +1 -1
  508. package/dist/modules/runtime_settings.js +15 -10
  509. package/dist/modules/runtime_settings.js.map +1 -1
  510. package/dist/modules/settings_merge.d.ts +1 -4
  511. package/dist/modules/settings_merge.d.ts.map +1 -1
  512. package/dist/modules/settings_merge.js +2 -1
  513. package/dist/modules/settings_merge.js.map +1 -1
  514. package/dist/modules/storage/browser_adapter.d.ts +7 -3
  515. package/dist/modules/storage/browser_adapter.d.ts.map +1 -1
  516. package/dist/modules/storage/browser_adapter.js +2 -0
  517. package/dist/modules/storage/browser_adapter.js.map +1 -1
  518. package/dist/modules/storage/index.d.ts +2 -2
  519. package/dist/modules/storage/index.d.ts.map +1 -1
  520. package/dist/modules/storage/index.js +2 -5
  521. package/dist/modules/storage/index.js.map +1 -1
  522. package/dist/modules/storage/node_adapter.d.ts +3 -3
  523. package/dist/modules/storage/node_adapter.d.ts.map +1 -1
  524. package/dist/modules/storage/node_adapter.js +9 -7
  525. package/dist/modules/storage/node_adapter.js.map +1 -1
  526. package/dist/modules/storage/types.d.ts +13 -10
  527. package/dist/modules/storage/types.d.ts.map +1 -1
  528. package/dist/modules/types.d.ts +0 -2
  529. package/dist/modules/types.d.ts.map +1 -1
  530. package/dist/modules/utils/base58check.d.ts +1 -9
  531. package/dist/modules/utils/base58check.d.ts.map +1 -1
  532. package/dist/modules/utils/base58check.js +14 -16
  533. package/dist/modules/utils/base58check.js.map +1 -1
  534. package/dist/modules/utils/build_dir.d.ts +2 -5
  535. package/dist/modules/utils/build_dir.d.ts.map +1 -1
  536. package/dist/modules/utils/build_dir.js +6 -3
  537. package/dist/modules/utils/build_dir.js.map +1 -1
  538. package/dist/modules/utils/errors.d.ts +2 -0
  539. package/dist/modules/utils/errors.d.ts.map +1 -0
  540. package/dist/modules/utils/errors.js +12 -0
  541. package/dist/modules/utils/errors.js.map +1 -0
  542. package/dist/modules/utils/fs_utils.d.ts +1 -8
  543. package/dist/modules/utils/fs_utils.d.ts.map +1 -1
  544. package/dist/modules/utils/fs_utils.js +8 -3
  545. package/dist/modules/utils/fs_utils.js.map +1 -1
  546. package/dist/modules/utils/math_utils.d.ts +2 -6
  547. package/dist/modules/utils/math_utils.d.ts.map +1 -1
  548. package/dist/modules/utils/math_utils.js +6 -2
  549. package/dist/modules/utils/math_utils.js.map +1 -1
  550. package/dist/modules/validate_profiles.d.ts +1 -11
  551. package/dist/modules/validate_profiles.d.ts.map +1 -1
  552. package/dist/modules/validate_profiles.js +25 -27
  553. package/dist/modules/validate_profiles.js.map +1 -1
  554. package/dist/pm2.d.ts +7 -6
  555. package/dist/pm2.d.ts.map +1 -1
  556. package/dist/pm2.js +132 -106
  557. package/dist/pm2.js.map +1 -1
  558. package/dist/scripts/analyze-git.d.ts.map +1 -1
  559. package/dist/scripts/analyze-git.js +15 -11
  560. package/dist/scripts/analyze-git.js.map +1 -1
  561. package/dist/scripts/analyze-orders.d.ts +146 -1
  562. package/dist/scripts/analyze-orders.d.ts.map +1 -1
  563. package/dist/scripts/analyze-orders.js +81 -105
  564. package/dist/scripts/analyze-orders.js.map +1 -1
  565. package/dist/scripts/diagnose-pool-history.d.ts.map +1 -1
  566. package/dist/scripts/diagnose-pool-history.js +9 -8
  567. package/dist/scripts/diagnose-pool-history.js.map +1 -1
  568. package/dist/scripts/divergence-calc.d.ts.map +1 -1
  569. package/dist/scripts/divergence-calc.js +11 -8
  570. package/dist/scripts/divergence-calc.js.map +1 -1
  571. package/dist/scripts/fix-err-message.d.ts +3 -0
  572. package/dist/scripts/fix-err-message.d.ts.map +1 -0
  573. package/dist/scripts/fix-err-message.js +198 -0
  574. package/dist/scripts/fix-err-message.js.map +1 -0
  575. package/dist/scripts/generate_lp_chart.d.ts +2 -9
  576. package/dist/scripts/generate_lp_chart.d.ts.map +1 -1
  577. package/dist/scripts/generate_lp_chart.js +14 -10
  578. package/dist/scripts/generate_lp_chart.js.map +1 -1
  579. package/dist/scripts/generate_mainnet_corpus_report.js +4 -4
  580. package/dist/scripts/generate_mainnet_corpus_report.js.map +1 -1
  581. package/dist/scripts/generate_market_adapter_whitelist.d.ts +17 -1
  582. package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +1 -1
  583. package/dist/scripts/generate_market_adapter_whitelist.js +23 -20
  584. package/dist/scripts/generate_market_adapter_whitelist.js.map +1 -1
  585. package/dist/scripts/migrate_bot_keys.d.ts +1 -4
  586. package/dist/scripts/migrate_bot_keys.d.ts.map +1 -1
  587. package/dist/scripts/migrate_bot_keys.js +48 -124
  588. package/dist/scripts/migrate_bot_keys.js.map +1 -1
  589. package/dist/scripts/native_release_gates.d.ts.map +1 -1
  590. package/dist/scripts/native_release_gates.js +18 -14
  591. package/dist/scripts/native_release_gates.js.map +1 -1
  592. package/dist/scripts/print_grid.js +1 -1
  593. package/dist/scripts/print_grid.js.map +1 -1
  594. package/dist/scripts/run-tests.js +1 -0
  595. package/dist/scripts/run-tests.js.map +1 -1
  596. package/dist/scripts/runner.d.ts +1 -11
  597. package/dist/scripts/runner.d.ts.map +1 -1
  598. package/dist/scripts/runner.js +21 -19
  599. package/dist/scripts/runner.js.map +1 -1
  600. package/dist/scripts/test-credit-renewal.d.ts.map +1 -1
  601. package/dist/scripts/test-credit-renewal.js +2 -1
  602. package/dist/scripts/test-credit-renewal.js.map +1 -1
  603. package/dist/scripts/update.d.ts.map +1 -1
  604. package/dist/scripts/update.js +85 -79
  605. package/dist/scripts/update.js.map +1 -1
  606. package/dist/scripts/validate_bots.js +2 -2
  607. package/dist/scripts/validate_bots.js.map +1 -1
  608. package/dist/scripts/verify-browser-bundle.js +25 -1
  609. package/dist/scripts/verify-browser-bundle.js.map +1 -1
  610. package/dist/unlock.d.ts +4 -17
  611. package/dist/unlock.d.ts.map +1 -1
  612. package/dist/unlock.js +141 -108
  613. package/dist/unlock.js.map +1 -1
  614. package/package.json +7 -2
  615. package/dist/analysis/ama_fitting/analyze_ama_price_changes.d.ts +0 -3
  616. package/dist/analysis/ama_fitting/analyze_ama_price_changes.d.ts.map +0 -1
  617. package/dist/analysis/ama_fitting/analyze_ama_price_changes.js +0 -149
  618. package/dist/analysis/ama_fitting/analyze_ama_price_changes.js.map +0 -1
  619. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts +0 -3
  620. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts.map +0 -1
  621. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js +0 -387
  622. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js.map +0 -1
  623. package/dist/analysis/ama_fitting/calibrate_convergence_er.d.ts +0 -3
  624. package/dist/analysis/ama_fitting/calibrate_convergence_er.d.ts.map +0 -1
  625. package/dist/analysis/ama_fitting/calibrate_convergence_er.js +0 -200
  626. package/dist/analysis/ama_fitting/calibrate_convergence_er.js.map +0 -1
  627. package/dist/analysis/ama_fitting/fetch_lp_candles.d.ts +0 -3
  628. package/dist/analysis/ama_fitting/fetch_lp_candles.d.ts.map +0 -1
  629. package/dist/analysis/ama_fitting/fetch_lp_candles.js +0 -212
  630. package/dist/analysis/ama_fitting/fetch_lp_candles.js.map +0 -1
  631. package/dist/analysis/ama_fitting/generate_unified_comparison_chart.d.ts +0 -31
  632. package/dist/analysis/ama_fitting/generate_unified_comparison_chart.d.ts.map +0 -1
  633. package/dist/analysis/ama_fitting/generate_unified_comparison_chart.js +0 -249
  634. package/dist/analysis/ama_fitting/generate_unified_comparison_chart.js.map +0 -1
  635. package/dist/analysis/ama_fitting/optimizer_high_resolution.d.ts +0 -2
  636. package/dist/analysis/ama_fitting/optimizer_high_resolution.d.ts.map +0 -1
  637. package/dist/analysis/ama_fitting/optimizer_high_resolution.js +0 -782
  638. package/dist/analysis/ama_fitting/optimizer_high_resolution.js.map +0 -1
  639. package/dist/analysis/analyze_derivatives.d.ts +0 -95
  640. package/dist/analysis/analyze_derivatives.d.ts.map +0 -1
  641. package/dist/analysis/analyze_derivatives.js +0 -295
  642. package/dist/analysis/analyze_derivatives.js.map +0 -1
  643. package/dist/analysis/analyze_dynamic_weight.d.ts +0 -14
  644. package/dist/analysis/analyze_dynamic_weight.d.ts.map +0 -1
  645. package/dist/analysis/analyze_dynamic_weight.js +0 -207
  646. package/dist/analysis/analyze_dynamic_weight.js.map +0 -1
  647. package/dist/analysis/analyze_kalman.d.ts +0 -13
  648. package/dist/analysis/analyze_kalman.d.ts.map +0 -1
  649. package/dist/analysis/analyze_kalman.js +0 -123
  650. package/dist/analysis/analyze_kalman.js.map +0 -1
  651. package/dist/analysis/analyze_regime.d.ts +0 -20
  652. package/dist/analysis/analyze_regime.d.ts.map +0 -1
  653. package/dist/analysis/analyze_regime.js +0 -139
  654. package/dist/analysis/analyze_regime.js.map +0 -1
  655. package/dist/analysis/analyze_regime_windows.d.ts +0 -19
  656. package/dist/analysis/analyze_regime_windows.d.ts.map +0 -1
  657. package/dist/analysis/analyze_regime_windows.js +0 -413
  658. package/dist/analysis/analyze_regime_windows.js.map +0 -1
  659. package/dist/analysis/analyze_risk_profile.d.ts +0 -3
  660. package/dist/analysis/analyze_risk_profile.d.ts.map +0 -1
  661. package/dist/analysis/analyze_risk_profile.js +0 -159
  662. package/dist/analysis/analyze_risk_profile.js.map +0 -1
  663. package/dist/analysis/analyze_trade_heatmap.d.ts +0 -3
  664. package/dist/analysis/analyze_trade_heatmap.d.ts.map +0 -1
  665. package/dist/analysis/analyze_trade_heatmap.js +0 -367
  666. package/dist/analysis/analyze_trade_heatmap.js.map +0 -1
  667. package/dist/analysis/analyze_volatility.d.ts +0 -22
  668. package/dist/analysis/analyze_volatility.d.ts.map +0 -1
  669. package/dist/analysis/analyze_volatility.js +0 -172
  670. package/dist/analysis/analyze_volatility.js.map +0 -1
  671. package/dist/analysis/bot_fitting/backtest_ama_sweep.d.ts +0 -99
  672. package/dist/analysis/bot_fitting/backtest_ama_sweep.d.ts.map +0 -1
  673. package/dist/analysis/bot_fitting/backtest_ama_sweep.js +0 -804
  674. package/dist/analysis/bot_fitting/backtest_ama_sweep.js.map +0 -1
  675. package/dist/analysis/bot_fitting/backtest_bot_fitting.d.ts +0 -2
  676. package/dist/analysis/bot_fitting/backtest_bot_fitting.d.ts.map +0 -1
  677. package/dist/analysis/bot_fitting/backtest_bot_fitting.js +0 -347
  678. package/dist/analysis/bot_fitting/backtest_bot_fitting.js.map +0 -1
  679. package/dist/analysis/bot_fitting/shared_utils.d.ts +0 -15
  680. package/dist/analysis/bot_fitting/shared_utils.d.ts.map +0 -1
  681. package/dist/analysis/bot_fitting/shared_utils.js +0 -48
  682. package/dist/analysis/bot_fitting/shared_utils.js.map +0 -1
  683. package/dist/analysis/bot_key_utils.d.ts +0 -22
  684. package/dist/analysis/bot_key_utils.d.ts.map +0 -1
  685. package/dist/analysis/bot_key_utils.js +0 -162
  686. package/dist/analysis/bot_key_utils.js.map +0 -1
  687. package/dist/analysis/bot_usage/discover_bot_accounts.d.ts +0 -3
  688. package/dist/analysis/bot_usage/discover_bot_accounts.d.ts.map +0 -1
  689. package/dist/analysis/bot_usage/discover_bot_accounts.js +0 -498
  690. package/dist/analysis/bot_usage/discover_bot_accounts.js.map +0 -1
  691. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +0 -546
  692. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +0 -1
  693. package/dist/analysis/bot_usage/kibana_bot_queries.js +0 -427
  694. package/dist/analysis/bot_usage/kibana_bot_queries.js.map +0 -1
  695. package/dist/analysis/derivative_chart_generator.d.ts +0 -18
  696. package/dist/analysis/derivative_chart_generator.d.ts.map +0 -1
  697. package/dist/analysis/derivative_chart_generator.js +0 -896
  698. package/dist/analysis/derivative_chart_generator.js.map +0 -1
  699. package/dist/analysis/price_sources.d.ts +0 -34
  700. package/dist/analysis/price_sources.d.ts.map +0 -1
  701. package/dist/analysis/price_sources.js +0 -103
  702. package/dist/analysis/price_sources.js.map +0 -1
  703. package/dist/analysis/trade_profitability.d.ts +0 -115
  704. package/dist/analysis/trade_profitability.d.ts.map +0 -1
  705. package/dist/analysis/trade_profitability.js +0 -1247
  706. package/dist/analysis/trade_profitability.js.map +0 -1
  707. package/dist/analysis/tradingview/analyze_tradingview.d.ts +0 -36
  708. package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +0 -1
  709. package/dist/analysis/tradingview/analyze_tradingview.js +0 -172
  710. package/dist/analysis/tradingview/analyze_tradingview.js.map +0 -1
  711. package/dist/analysis/trend_detection/derivative_analyzer.d.ts +0 -250
  712. package/dist/analysis/trend_detection/derivative_analyzer.d.ts.map +0 -1
  713. package/dist/analysis/trend_detection/derivative_analyzer.js +0 -903
  714. package/dist/analysis/trend_detection/derivative_analyzer.js.map +0 -1
  715. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts +0 -6
  716. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +0 -1
  717. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +0 -1450
  718. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +0 -1
  719. package/dist/analysis/trend_detection/kalman_chart_generator.d.ts +0 -6
  720. package/dist/analysis/trend_detection/kalman_chart_generator.d.ts.map +0 -1
  721. package/dist/analysis/trend_detection/kalman_chart_generator.js +0 -408
  722. package/dist/analysis/trend_detection/kalman_chart_generator.js.map +0 -1
  723. package/dist/analysis/trend_detection/regime_chart_generator.d.ts +0 -6
  724. package/dist/analysis/trend_detection/regime_chart_generator.d.ts.map +0 -1
  725. package/dist/analysis/trend_detection/regime_chart_generator.js +0 -345
  726. package/dist/analysis/trend_detection/regime_chart_generator.js.map +0 -1
  727. package/dist/analysis/trend_detection/tests/test_kalman_trend.d.ts +0 -2
  728. package/dist/analysis/trend_detection/tests/test_kalman_trend.d.ts.map +0 -1
  729. package/dist/analysis/trend_detection/tests/test_kalman_trend.js +0 -130
  730. package/dist/analysis/trend_detection/tests/test_kalman_trend.js.map +0 -1
  731. package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.d.ts +0 -2
  732. package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.d.ts.map +0 -1
  733. package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.js +0 -38
  734. package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.js.map +0 -1
  735. package/dist/analysis/trend_detection/volatility_chart_generator.d.ts +0 -6
  736. package/dist/analysis/trend_detection/volatility_chart_generator.d.ts.map +0 -1
  737. package/dist/analysis/trend_detection/volatility_chart_generator.js +0 -732
  738. package/dist/analysis/trend_detection/volatility_chart_generator.js.map +0 -1
@@ -53,35 +53,66 @@
53
53
  *
54
54
  * ===============================================================================
55
55
  */
56
- const { path } = require('./path_api');
57
- const { BitShares, waitForConnected, onReconnect: registerReconnectHook } = require('./bitshares_client');
58
- const { getStorage } = require('./storage');
59
- const storage = getStorage();
60
- const chainKeys = require('./chain_keys');
61
- const { getKeyStore } = require('./key_store');
62
- const chainOrders = require('./chain_orders');
63
- const fundRegistry = require('./fund_registry');
64
- const { BroadcastUncertainError } = require('./dexbot_credential_client');
65
- const { OrderManager, grid: Grid } = require('./order');
66
- const { retryPersistenceIfNeeded, initializeFeeCache, } = require('./order/utils/system');
67
- const { hasExecutableActions, validateCreateTargetSlots } = require('./order/utils/validate');
68
- const { buildCreateOrderArgs, buildCreateOpFingerprint, virtualizeOrder, correctAllPriceMismatches, convertToSpreadPlaceholder, buildOutsideInPairGroups, extractBatchOperationResults, buildFillKey, formatUnmatchedChainOrder, parseChainOrder } = require('./order/utils/order');
69
- const { validateOrderSize } = require('./order/utils/math');
70
- const { ProcessedFillStore, PROCESSED_FILL_PERSISTENCE_MODES } = require('./order/processed_fill_store');
71
- const DexbotFillRuntime = require('./dexbot_fill_runtime');
72
- const DexbotMaintenanceRuntime = require('./dexbot_maintenance_runtime');
73
- const CreditRuntime = require('./credit_runtime');
74
- const { ORDER_STATES, ORDER_TYPES, REBALANCE_STATES, COW_ACTIONS, TIMING, PIPELINE_TIMING, GRID_LIMITS, MAINTENANCE, FILL_PROCESSING, DAEMON_CODES, } = require('./constants');
75
- const { PATHS, getRecalculateTriggerFile } = require('./paths');
76
- const { attemptResumePersistedGridByPriceMatch, decideStartupGridAction, reconcileGridOrders } = require('./order/grid_reconcile');
77
- const { AccountOrders } = require('./account_orders');
78
- const { parseJsonWithComments } = require('./order/utils/system');
79
- const { cloneWeightDistribution } = require('./order/utils/math');
80
- const { normalizeBotEntry } = require('./bot_settings');
81
- const Format = require('./order/format');
82
- const { resolveBotRuntimeSettings } = require('./runtime_settings');
83
- const PROFILES_BOTS_FILE = PATHS.PROFILES.BOTS_JSON;
84
- const PROFILES_DIR = PATHS.PROFILES_DIR;
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
- this.manager._recentFillKeysSnapshot = this._getRecentFillKeysSnapshot();
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
- const snapshot = {};
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
- this.manager?._cleanExpiredLocks?.();
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
- this._lastGridActivityAt = Date.now();
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
- if (!this.manager)
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
- const illegalSignal = this.manager?.consumeIllegalStateSignal?.();
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
- const baseResult = { executed: false, hadRotation: false };
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
- if (!this.manager || !Array.isArray(updates) || updates.length === 0) {
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
- const staleIds = Array.from(staleOrderIds || []).filter(Boolean);
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
- // Try a targeted fix first: extract the affected order IDs from the
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
- if (!Array.isArray(opContexts))
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
- if (!this.accountOrders || !this.manager) {
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
- if (!this.manager?.checkFundDriftAfterFills)
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
- try {
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
- // Create AccountOrders with bot-specific file (one file per bot)
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 DexbotMaintenanceRuntime.refreshDynamicWeightDistribution(this, context);
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
- let { persistedGrid, persistedBtsFeesOwed, persistedBoundaryIdx, persistedBtsBalance, persistedRecentFillKeys, } = startupState;
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
- if (!this.accountId || this.config?.dryRun) {
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.config.fillProcessing?.MAX_CONSECUTIVE_CONSUMER_FAILURES ?? FILL_PROCESSING.MAX_CONSECUTIVE_CONSUMER_FAILURES;
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
- const fp = this.config.fillProcessing || FILL_PROCESSING;
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
- const failures = this._consecutiveConsumeFailures;
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
- // Helper: every early return below is a "deferral", not a failure.
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
- if (this._incomingFillQueue.length === 0)
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.message}. Falling back to interactive authentication.`);
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.message}`);
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
- if (!this.manager) {
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 buildOutsideInPairGroups(orders, {
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 buildOutsideInPairGroups(createEntries, {
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
- const extracted = extractBatchOperationResults(result);
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
- const missing = [];
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
- try {
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
- if (!Array.isArray(blockers) || blockers.length === 0 || !this.manager)
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
- const blockers = Array.isArray(missingCreateResults)
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 formatUnmatchedChainOrder(order);
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
- if (!this.manager || !entry || !entry.order)
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
- if (this.manager && this.manager._pendingBroadcasts instanceof Map) {
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
- if (!chainOrder || !slotId)
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
- if (!chainOrder)
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
- if (!Array.isArray(chainOrders) || !slotId)
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
- if (this.manager?._fillProcessingLock &&
2817
- typeof this.manager._fillProcessingLock.acquire === 'function' &&
2818
- !this.manager._fillProcessingLock.isReentrant()) {
2819
- return this.manager._fillProcessingLock.acquire(() => this._reconcileAfterUncertainBroadcast(err, opContexts, options));
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
- const cycleId = this._currentCycleId || 0;
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
- if (!Array.isArray(opContexts) || opContexts.length < 2)
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
- const MAX_RETRIES = 1;
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
- if (!this._shouldExecuteCreatePairMode(opContexts)) {
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
- if (!operations || operations.length === 0) {
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
- const candidates = [
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 validateOrderSize(size, type, this.manager.assets, this.config.gridLimits?.MIN_ORDER_SIZE_FACTOR, this._resolveIdealSizeForValidation(orderLike, fallbackSize), this.config.gridLimits?.PARTIAL_DUST_THRESHOLD_PERCENTAGE);
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.message}`;
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.message || '');
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.message}`, 'error');
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.message}`);
3659
- this.manager?.logger?.log?.(`[CREDENTIAL] Credential recovery sync failed: ${err.message}. Writes remain guarded by preflight.`, 'error');
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.message || '');
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.message}. ${hint}`, 'error');
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.message}`);
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.message}`, 'warn');
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
- const normalizedPlan = Array.isArray(plan)
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
- const { WorkingGrid } = require('./order/working_grid');
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
- if (!workingGrid || !skippedSlotIds || skippedSlotIds.size === 0) {
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
- this._currentCycleId = (Number.isFinite(Number(this._currentCycleId)) ? Number(this._currentCycleId) : 0) + 1;
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
- const results = this._extractOperationResults(result, '_processBatchResults');
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 DexbotMaintenanceRuntime.performGridResync(this, options);
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 DexbotMaintenanceRuntime.handlePendingTriggerReset(this);
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 DexbotMaintenanceRuntime.setupTriggerFileDetection(this);
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.message}`);
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.message}`);
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 DexbotMaintenanceRuntime.performPeriodicGridChecks(this);
1531
+ return dexbot_maintenance_runtime_1.default.performPeriodicGridChecks(this);
4800
1532
  }
4801
1533
  _isOpenOrdersSyncLoopEnabled() {
4802
- return DexbotMaintenanceRuntime.isOpenOrdersSyncLoopEnabled(this);
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 DexbotMaintenanceRuntime.startOpenOrdersSyncLoop(this);
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 DexbotMaintenanceRuntime.stopOpenOrdersSyncLoop(this);
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 DexbotMaintenanceRuntime.setupBlockchainFetchInterval(this);
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 DexbotMaintenanceRuntime.stopBlockchainFetchInterval(this);
1564
+ return dexbot_maintenance_runtime_1.default.stopBlockchainFetchInterval(this);
4833
1565
  }
4834
1566
  async _releaseMarketAdapterRuntime(context = 'shutdown') {
4835
- return DexbotMaintenanceRuntime.releaseMarketAdapterRuntime(this, this.config?.botKey || this.config?.name, context);
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 CreditRuntime(this, {
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.message}`);
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.message}`);
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
- if (!this.manager || typeof this._performGridResync !== 'function') {
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
- if (!this.manager || this.manager.requestStructuralGridResync)
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
- this.manager?._cleanExpiredLocks?.();
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 DexbotMaintenanceRuntime.executeMaintenanceLogic(this, context);
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 DexbotMaintenanceRuntime.cancelDustOrders(this, { buy: buyDust, sell: sellDust });
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
- if (this._shuttingDown || !this.manager)
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
- this._dustHealthCheckTimer = setInterval(() => {
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 DexbotMaintenanceRuntime.runGridMaintenance(this, context, options);
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.message}`);
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.message}`);
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.message}`);
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.message}`);
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.message}`);
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.message}`);
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.message}`);
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.message}`);
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.message}`);
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.message}`);
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.message}`);
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
- module.exports = Object.assign(DEXBot, {
5371
- normalizeBotEntry: require('./bot_settings').normalizeBotEntry
5372
- });
1991
+ DEXBot.normalizeBotEntry = bot_settings_1.normalizeBotEntry;
1992
+ exports.default = DEXBot;
1993
+ module.exports = DEXBot;
5373
1994
  //# sourceMappingURL=dexbot_class.js.map