@waiaas/daemon 2.11.0-rc.8 → 2.11.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 (414) hide show
  1. package/README.md +5 -5
  2. package/dist/api/middleware/address-validation.d.ts +6 -33
  3. package/dist/api/middleware/address-validation.d.ts.map +1 -1
  4. package/dist/api/middleware/address-validation.js +5 -129
  5. package/dist/api/middleware/address-validation.js.map +1 -1
  6. package/dist/api/middleware/host-guard.d.ts +1 -1
  7. package/dist/api/middleware/host-guard.js +2 -2
  8. package/dist/api/middleware/host-guard.js.map +1 -1
  9. package/dist/api/middleware/index.d.ts +1 -0
  10. package/dist/api/middleware/index.d.ts.map +1 -1
  11. package/dist/api/middleware/index.js +1 -0
  12. package/dist/api/middleware/index.js.map +1 -1
  13. package/dist/api/middleware/master-auth.d.ts +2 -5
  14. package/dist/api/middleware/master-auth.d.ts.map +1 -1
  15. package/dist/api/middleware/master-auth.js.map +1 -1
  16. package/dist/api/middleware/rate-limiter.d.ts +51 -0
  17. package/dist/api/middleware/rate-limiter.d.ts.map +1 -0
  18. package/dist/api/middleware/rate-limiter.js +146 -0
  19. package/dist/api/middleware/rate-limiter.js.map +1 -0
  20. package/dist/api/middleware/siwe-verify.d.ts +6 -26
  21. package/dist/api/middleware/siwe-verify.d.ts.map +1 -1
  22. package/dist/api/middleware/siwe-verify.js +5 -50
  23. package/dist/api/middleware/siwe-verify.js.map +1 -1
  24. package/dist/api/routes/actions.d.ts +1 -0
  25. package/dist/api/routes/actions.d.ts.map +1 -1
  26. package/dist/api/routes/actions.js +52 -4
  27. package/dist/api/routes/actions.js.map +1 -1
  28. package/dist/api/routes/admin-actions.d.ts +1 -0
  29. package/dist/api/routes/admin-actions.d.ts.map +1 -1
  30. package/dist/api/routes/admin-actions.js +3 -3
  31. package/dist/api/routes/admin-actions.js.map +1 -1
  32. package/dist/api/routes/admin-auth.d.ts.map +1 -1
  33. package/dist/api/routes/admin-auth.js +12 -7
  34. package/dist/api/routes/admin-auth.js.map +1 -1
  35. package/dist/api/routes/admin-credentials.js +2 -2
  36. package/dist/api/routes/admin-credentials.js.map +1 -1
  37. package/dist/api/routes/admin-monitoring.d.ts +10 -0
  38. package/dist/api/routes/admin-monitoring.d.ts.map +1 -1
  39. package/dist/api/routes/admin-monitoring.js +59 -14
  40. package/dist/api/routes/admin-monitoring.js.map +1 -1
  41. package/dist/api/routes/admin-notifications.d.ts.map +1 -1
  42. package/dist/api/routes/admin-notifications.js +2 -15
  43. package/dist/api/routes/admin-notifications.js.map +1 -1
  44. package/dist/api/routes/admin-settings.d.ts.map +1 -1
  45. package/dist/api/routes/admin-settings.js +90 -1
  46. package/dist/api/routes/admin-settings.js.map +1 -1
  47. package/dist/api/routes/admin-wallets.d.ts +16 -1
  48. package/dist/api/routes/admin-wallets.d.ts.map +1 -1
  49. package/dist/api/routes/admin-wallets.js +64 -75
  50. package/dist/api/routes/admin-wallets.js.map +1 -1
  51. package/dist/api/routes/admin.d.ts +1 -0
  52. package/dist/api/routes/admin.d.ts.map +1 -1
  53. package/dist/api/routes/admin.js.map +1 -1
  54. package/dist/api/routes/credentials.js +2 -2
  55. package/dist/api/routes/credentials.js.map +1 -1
  56. package/dist/api/routes/defi-positions.js.map +1 -1
  57. package/dist/api/routes/nfts.js.map +1 -1
  58. package/dist/api/routes/openapi-schemas.d.ts +412 -12
  59. package/dist/api/routes/openapi-schemas.d.ts.map +1 -1
  60. package/dist/api/routes/openapi-schemas.js +38 -5
  61. package/dist/api/routes/openapi-schemas.js.map +1 -1
  62. package/dist/api/routes/policies.d.ts +2 -0
  63. package/dist/api/routes/policies.d.ts.map +1 -1
  64. package/dist/api/routes/policies.js +55 -6
  65. package/dist/api/routes/policies.js.map +1 -1
  66. package/dist/api/routes/rpc-proxy.js.map +1 -1
  67. package/dist/api/routes/sessions.d.ts.map +1 -1
  68. package/dist/api/routes/sessions.js +47 -28
  69. package/dist/api/routes/sessions.js.map +1 -1
  70. package/dist/api/routes/staking.d.ts.map +1 -1
  71. package/dist/api/routes/staking.js +4 -76
  72. package/dist/api/routes/staking.js.map +1 -1
  73. package/dist/api/routes/tokens.d.ts.map +1 -1
  74. package/dist/api/routes/tokens.js.map +1 -1
  75. package/dist/api/routes/transactions.d.ts +1 -0
  76. package/dist/api/routes/transactions.d.ts.map +1 -1
  77. package/dist/api/routes/transactions.js +8 -2
  78. package/dist/api/routes/transactions.js.map +1 -1
  79. package/dist/api/routes/userop.d.ts.map +1 -1
  80. package/dist/api/routes/userop.js +0 -2
  81. package/dist/api/routes/userop.js.map +1 -1
  82. package/dist/api/routes/wallet-apps.d.ts.map +1 -1
  83. package/dist/api/routes/wallet-apps.js +20 -13
  84. package/dist/api/routes/wallet-apps.js.map +1 -1
  85. package/dist/api/routes/wallet.js.map +1 -1
  86. package/dist/api/routes/wallets.d.ts.map +1 -1
  87. package/dist/api/routes/wallets.js +3 -0
  88. package/dist/api/routes/wallets.js.map +1 -1
  89. package/dist/api/routes/wc.d.ts.map +1 -1
  90. package/dist/api/routes/wc.js +13 -8
  91. package/dist/api/routes/wc.js.map +1 -1
  92. package/dist/api/routes/x402.d.ts.map +1 -1
  93. package/dist/api/routes/x402.js +1 -2
  94. package/dist/api/routes/x402.js.map +1 -1
  95. package/dist/api/server.d.ts +8 -4
  96. package/dist/api/server.d.ts.map +1 -1
  97. package/dist/api/server.js +46 -5
  98. package/dist/api/server.js.map +1 -1
  99. package/dist/constants.d.ts +1 -1
  100. package/dist/constants.d.ts.map +1 -1
  101. package/dist/constants.js +1 -1
  102. package/dist/constants.js.map +1 -1
  103. package/dist/infrastructure/action/action-provider-registry.d.ts.map +1 -1
  104. package/dist/infrastructure/action/action-provider-registry.js +2 -3
  105. package/dist/infrastructure/action/action-provider-registry.js.map +1 -1
  106. package/dist/infrastructure/action/builtin-metadata.d.ts +22 -0
  107. package/dist/infrastructure/action/builtin-metadata.d.ts.map +1 -0
  108. package/dist/infrastructure/action/builtin-metadata.js +29 -0
  109. package/dist/infrastructure/action/builtin-metadata.js.map +1 -0
  110. package/dist/infrastructure/adapter-pool.d.ts +2 -1
  111. package/dist/infrastructure/adapter-pool.d.ts.map +1 -1
  112. package/dist/infrastructure/adapter-pool.js.map +1 -1
  113. package/dist/infrastructure/auth/address-validation.d.ts +38 -0
  114. package/dist/infrastructure/auth/address-validation.d.ts.map +1 -0
  115. package/dist/infrastructure/auth/address-validation.js +134 -0
  116. package/dist/infrastructure/auth/address-validation.js.map +1 -0
  117. package/dist/infrastructure/auth/siwe-verify.d.ts +34 -0
  118. package/dist/infrastructure/auth/siwe-verify.d.ts.map +1 -0
  119. package/dist/infrastructure/auth/siwe-verify.js +58 -0
  120. package/dist/infrastructure/auth/siwe-verify.js.map +1 -0
  121. package/dist/infrastructure/auth/types.d.ts +12 -0
  122. package/dist/infrastructure/auth/types.d.ts.map +1 -0
  123. package/dist/infrastructure/auth/types.js +8 -0
  124. package/dist/infrastructure/auth/types.js.map +1 -0
  125. package/dist/infrastructure/config/loader.d.ts +1 -10
  126. package/dist/infrastructure/config/loader.d.ts.map +1 -1
  127. package/dist/infrastructure/config/loader.js +0 -2
  128. package/dist/infrastructure/config/loader.js.map +1 -1
  129. package/dist/infrastructure/database/migrate.d.ts +6 -18
  130. package/dist/infrastructure/database/migrate.d.ts.map +1 -1
  131. package/dist/infrastructure/database/migrate.js +25 -2856
  132. package/dist/infrastructure/database/migrate.js.map +1 -1
  133. package/dist/infrastructure/database/migrations/v11-v20.d.ts +17 -0
  134. package/dist/infrastructure/database/migrations/v11-v20.d.ts.map +1 -0
  135. package/dist/infrastructure/database/migrations/v11-v20.js +295 -0
  136. package/dist/infrastructure/database/migrations/v11-v20.js.map +1 -0
  137. package/dist/infrastructure/database/migrations/v2-v10.d.ts +16 -0
  138. package/dist/infrastructure/database/migrations/v2-v10.d.ts.map +1 -0
  139. package/dist/infrastructure/database/migrations/v2-v10.js +539 -0
  140. package/dist/infrastructure/database/migrations/v2-v10.js.map +1 -0
  141. package/dist/infrastructure/database/migrations/v21-v30.d.ts +17 -0
  142. package/dist/infrastructure/database/migrations/v21-v30.d.ts.map +1 -0
  143. package/dist/infrastructure/database/migrations/v21-v30.js +507 -0
  144. package/dist/infrastructure/database/migrations/v21-v30.js.map +1 -0
  145. package/dist/infrastructure/database/migrations/v31-v40.d.ts +17 -0
  146. package/dist/infrastructure/database/migrations/v31-v40.d.ts.map +1 -0
  147. package/dist/infrastructure/database/migrations/v31-v40.js +203 -0
  148. package/dist/infrastructure/database/migrations/v31-v40.js.map +1 -0
  149. package/dist/infrastructure/database/migrations/v41-v50.d.ts +17 -0
  150. package/dist/infrastructure/database/migrations/v41-v50.d.ts.map +1 -0
  151. package/dist/infrastructure/database/migrations/v41-v50.js +188 -0
  152. package/dist/infrastructure/database/migrations/v41-v50.js.map +1 -0
  153. package/dist/infrastructure/database/migrations/v51-v59.d.ts +17 -0
  154. package/dist/infrastructure/database/migrations/v51-v59.d.ts.map +1 -0
  155. package/dist/infrastructure/database/migrations/v51-v59.js +420 -0
  156. package/dist/infrastructure/database/migrations/v51-v59.js.map +1 -0
  157. package/dist/infrastructure/database/schema-ddl.d.ts +24 -0
  158. package/dist/infrastructure/database/schema-ddl.d.ts.map +1 -0
  159. package/dist/infrastructure/database/schema-ddl.js +596 -0
  160. package/dist/infrastructure/database/schema-ddl.js.map +1 -0
  161. package/dist/infrastructure/database/schema.d.ts +38 -0
  162. package/dist/infrastructure/database/schema.d.ts.map +1 -1
  163. package/dist/infrastructure/database/schema.js +2 -0
  164. package/dist/infrastructure/database/schema.js.map +1 -1
  165. package/dist/infrastructure/jwt/jwt-secret-manager.d.ts.map +1 -1
  166. package/dist/infrastructure/jwt/jwt-secret-manager.js +16 -3
  167. package/dist/infrastructure/jwt/jwt-secret-manager.js.map +1 -1
  168. package/dist/infrastructure/nft/alchemy-nft-indexer.d.ts.map +1 -1
  169. package/dist/infrastructure/nft/alchemy-nft-indexer.js +0 -1
  170. package/dist/infrastructure/nft/alchemy-nft-indexer.js.map +1 -1
  171. package/dist/infrastructure/nft/helius-nft-indexer.d.ts.map +1 -1
  172. package/dist/infrastructure/nft/helius-nft-indexer.js +1 -2
  173. package/dist/infrastructure/nft/helius-nft-indexer.js.map +1 -1
  174. package/dist/infrastructure/nft/nft-indexer-client.d.ts.map +1 -1
  175. package/dist/infrastructure/nft/nft-indexer-client.js +0 -2
  176. package/dist/infrastructure/nft/nft-indexer-client.js.map +1 -1
  177. package/dist/infrastructure/security/ssrf-guard.d.ts +33 -0
  178. package/dist/infrastructure/security/ssrf-guard.d.ts.map +1 -0
  179. package/dist/infrastructure/security/ssrf-guard.js +244 -0
  180. package/dist/infrastructure/security/ssrf-guard.js.map +1 -0
  181. package/dist/infrastructure/settings/hot-reload.d.ts +1 -1
  182. package/dist/infrastructure/settings/hot-reload.d.ts.map +1 -1
  183. package/dist/infrastructure/settings/hot-reload.js +0 -2
  184. package/dist/infrastructure/settings/hot-reload.js.map +1 -1
  185. package/dist/infrastructure/settings/index.d.ts +2 -2
  186. package/dist/infrastructure/settings/index.d.ts.map +1 -1
  187. package/dist/infrastructure/settings/index.js +1 -1
  188. package/dist/infrastructure/settings/index.js.map +1 -1
  189. package/dist/infrastructure/settings/setting-keys.d.ts +14 -0
  190. package/dist/infrastructure/settings/setting-keys.d.ts.map +1 -1
  191. package/dist/infrastructure/settings/setting-keys.js +296 -214
  192. package/dist/infrastructure/settings/setting-keys.js.map +1 -1
  193. package/dist/infrastructure/settings/settings-service.d.ts +6 -1
  194. package/dist/infrastructure/settings/settings-service.d.ts.map +1 -1
  195. package/dist/infrastructure/settings/settings-service.js +15 -5
  196. package/dist/infrastructure/settings/settings-service.js.map +1 -1
  197. package/dist/infrastructure/telegram/telegram-bot-service.d.ts.map +1 -1
  198. package/dist/infrastructure/telegram/telegram-bot-service.js +3 -2
  199. package/dist/infrastructure/telegram/telegram-bot-service.js.map +1 -1
  200. package/dist/infrastructure/token-registry/builtin-tokens.d.ts.map +1 -1
  201. package/dist/infrastructure/token-registry/builtin-tokens.js +4 -7
  202. package/dist/infrastructure/token-registry/builtin-tokens.js.map +1 -1
  203. package/dist/lifecycle/daemon-pipeline.d.ts +49 -0
  204. package/dist/lifecycle/daemon-pipeline.d.ts.map +1 -0
  205. package/dist/lifecycle/daemon-pipeline.js +281 -0
  206. package/dist/lifecycle/daemon-pipeline.js.map +1 -0
  207. package/dist/lifecycle/daemon-shutdown.d.ts +14 -0
  208. package/dist/lifecycle/daemon-shutdown.d.ts.map +1 -0
  209. package/dist/lifecycle/daemon-shutdown.js +176 -0
  210. package/dist/lifecycle/daemon-shutdown.js.map +1 -0
  211. package/dist/lifecycle/daemon-startup.d.ts +15 -0
  212. package/dist/lifecycle/daemon-startup.d.ts.map +1 -0
  213. package/dist/lifecycle/daemon-startup.js +1527 -0
  214. package/dist/lifecycle/daemon-startup.js.map +1 -0
  215. package/dist/lifecycle/daemon.d.ts +171 -114
  216. package/dist/lifecycle/daemon.d.ts.map +1 -1
  217. package/dist/lifecycle/daemon.js +22 -1904
  218. package/dist/lifecycle/daemon.js.map +1 -1
  219. package/dist/notifications/channels/discord.d.ts.map +1 -1
  220. package/dist/notifications/channels/discord.js +1 -0
  221. package/dist/notifications/channels/discord.js.map +1 -1
  222. package/dist/notifications/channels/slack.d.ts.map +1 -1
  223. package/dist/notifications/channels/slack.js +1 -0
  224. package/dist/notifications/channels/slack.js.map +1 -1
  225. package/dist/notifications/index.d.ts +0 -1
  226. package/dist/notifications/index.d.ts.map +1 -1
  227. package/dist/notifications/index.js +0 -1
  228. package/dist/notifications/index.js.map +1 -1
  229. package/dist/notifications/notification-service.d.ts.map +1 -1
  230. package/dist/notifications/notification-service.js +8 -6
  231. package/dist/notifications/notification-service.js.map +1 -1
  232. package/dist/pipeline/database-policy-engine.d.ts +18 -438
  233. package/dist/pipeline/database-policy-engine.d.ts.map +1 -1
  234. package/dist/pipeline/database-policy-engine.js +154 -1321
  235. package/dist/pipeline/database-policy-engine.js.map +1 -1
  236. package/dist/pipeline/dry-run.d.ts +5 -2
  237. package/dist/pipeline/dry-run.d.ts.map +1 -1
  238. package/dist/pipeline/dry-run.js +102 -8
  239. package/dist/pipeline/dry-run.js.map +1 -1
  240. package/dist/pipeline/evaluators/allowed-tokens.d.ts +28 -0
  241. package/dist/pipeline/evaluators/allowed-tokens.d.ts.map +1 -0
  242. package/dist/pipeline/evaluators/allowed-tokens.js +129 -0
  243. package/dist/pipeline/evaluators/allowed-tokens.js.map +1 -0
  244. package/dist/pipeline/evaluators/approved-spenders.d.ts +26 -0
  245. package/dist/pipeline/evaluators/approved-spenders.d.ts.map +1 -0
  246. package/dist/pipeline/evaluators/approved-spenders.js +115 -0
  247. package/dist/pipeline/evaluators/approved-spenders.js.map +1 -0
  248. package/dist/pipeline/evaluators/contract-whitelist.d.ts +28 -0
  249. package/dist/pipeline/evaluators/contract-whitelist.d.ts.map +1 -0
  250. package/dist/pipeline/evaluators/contract-whitelist.js +168 -0
  251. package/dist/pipeline/evaluators/contract-whitelist.js.map +1 -0
  252. package/dist/pipeline/evaluators/helpers.d.ts +9 -0
  253. package/dist/pipeline/evaluators/helpers.d.ts.map +1 -0
  254. package/dist/pipeline/evaluators/helpers.js +13 -0
  255. package/dist/pipeline/evaluators/helpers.js.map +1 -0
  256. package/dist/pipeline/evaluators/lending-asset-whitelist.d.ts +18 -0
  257. package/dist/pipeline/evaluators/lending-asset-whitelist.d.ts.map +1 -0
  258. package/dist/pipeline/evaluators/lending-asset-whitelist.js +44 -0
  259. package/dist/pipeline/evaluators/lending-asset-whitelist.js.map +1 -0
  260. package/dist/pipeline/evaluators/lending-ltv-limit.d.ts +24 -0
  261. package/dist/pipeline/evaluators/lending-ltv-limit.d.ts.map +1 -0
  262. package/dist/pipeline/evaluators/lending-ltv-limit.js +130 -0
  263. package/dist/pipeline/evaluators/lending-ltv-limit.js.map +1 -0
  264. package/dist/pipeline/evaluators/spending-limit.d.ts +46 -0
  265. package/dist/pipeline/evaluators/spending-limit.d.ts.map +1 -0
  266. package/dist/pipeline/evaluators/spending-limit.js +241 -0
  267. package/dist/pipeline/evaluators/spending-limit.js.map +1 -0
  268. package/dist/pipeline/evaluators/types.d.ts +71 -0
  269. package/dist/pipeline/evaluators/types.d.ts.map +1 -0
  270. package/dist/pipeline/evaluators/types.js +7 -0
  271. package/dist/pipeline/evaluators/types.js.map +1 -0
  272. package/dist/pipeline/external-action-pipeline.js.map +1 -1
  273. package/dist/pipeline/gas-condition-tracker.d.ts +1 -1
  274. package/dist/pipeline/gas-condition-tracker.js +1 -1
  275. package/dist/pipeline/pipeline-helpers.d.ts +146 -0
  276. package/dist/pipeline/pipeline-helpers.d.ts.map +1 -0
  277. package/dist/pipeline/pipeline-helpers.js +260 -0
  278. package/dist/pipeline/pipeline-helpers.js.map +1 -0
  279. package/dist/pipeline/pipeline.d.ts +1 -0
  280. package/dist/pipeline/pipeline.d.ts.map +1 -1
  281. package/dist/pipeline/pipeline.js +3 -2
  282. package/dist/pipeline/pipeline.js.map +1 -1
  283. package/dist/pipeline/resolve-effective-amount-usd.d.ts.map +1 -1
  284. package/dist/pipeline/resolve-effective-amount-usd.js +4 -10
  285. package/dist/pipeline/resolve-effective-amount-usd.js.map +1 -1
  286. package/dist/pipeline/sign-message.js +1 -1
  287. package/dist/pipeline/sign-message.js.map +1 -1
  288. package/dist/pipeline/sleep.d.ts +1 -5
  289. package/dist/pipeline/sleep.d.ts.map +1 -1
  290. package/dist/pipeline/sleep.js +2 -7
  291. package/dist/pipeline/sleep.js.map +1 -1
  292. package/dist/pipeline/stage1-validate.d.ts +8 -0
  293. package/dist/pipeline/stage1-validate.d.ts.map +1 -0
  294. package/dist/pipeline/stage1-validate.js +69 -0
  295. package/dist/pipeline/stage1-validate.js.map +1 -0
  296. package/dist/pipeline/stage2-auth.d.ts +12 -0
  297. package/dist/pipeline/stage2-auth.d.ts.map +1 -0
  298. package/dist/pipeline/stage2-auth.js +18 -0
  299. package/dist/pipeline/stage2-auth.js.map +1 -0
  300. package/dist/pipeline/stage3-policy.d.ts +26 -0
  301. package/dist/pipeline/stage3-policy.d.ts.map +1 -0
  302. package/dist/pipeline/stage3-policy.js +384 -0
  303. package/dist/pipeline/stage3-policy.js.map +1 -0
  304. package/dist/pipeline/stage4-wait.d.ts +8 -0
  305. package/dist/pipeline/stage4-wait.d.ts.map +1 -0
  306. package/dist/pipeline/stage4-wait.js +87 -0
  307. package/dist/pipeline/stage4-wait.js.map +1 -0
  308. package/dist/pipeline/stage5-execute.d.ts +120 -0
  309. package/dist/pipeline/stage5-execute.d.ts.map +1 -0
  310. package/dist/pipeline/stage5-execute.js +1070 -0
  311. package/dist/pipeline/stage5-execute.js.map +1 -0
  312. package/dist/pipeline/stage6-confirm.d.ts +11 -0
  313. package/dist/pipeline/stage6-confirm.d.ts.map +1 -0
  314. package/dist/pipeline/stage6-confirm.js +110 -0
  315. package/dist/pipeline/stage6-confirm.js.map +1 -0
  316. package/dist/pipeline/stages.d.ts +11 -245
  317. package/dist/pipeline/stages.d.ts.map +1 -1
  318. package/dist/pipeline/stages.js +11 -1896
  319. package/dist/pipeline/stages.js.map +1 -1
  320. package/dist/rpc-proxy/sync-pipeline.js +2 -2
  321. package/dist/rpc-proxy/sync-pipeline.js.map +1 -1
  322. package/dist/services/autostop/autostop-service.d.ts +4 -1
  323. package/dist/services/autostop/autostop-service.d.ts.map +1 -1
  324. package/dist/services/autostop/autostop-service.js +27 -7
  325. package/dist/services/autostop/autostop-service.js.map +1 -1
  326. package/dist/services/defi/position-tracker.d.ts +5 -0
  327. package/dist/services/defi/position-tracker.d.ts.map +1 -1
  328. package/dist/services/defi/position-tracker.js +41 -6
  329. package/dist/services/defi/position-tracker.js.map +1 -1
  330. package/dist/services/defi/position-write-queue.d.ts.map +1 -1
  331. package/dist/services/defi/position-write-queue.js +3 -2
  332. package/dist/services/defi/position-write-queue.js.map +1 -1
  333. package/dist/services/incoming/__tests__/integration-wiring.test.js +58 -0
  334. package/dist/services/incoming/__tests__/integration-wiring.test.js.map +1 -1
  335. package/dist/services/incoming/incoming-tx-monitor-service.d.ts.map +1 -1
  336. package/dist/services/incoming/incoming-tx-monitor-service.js +11 -14
  337. package/dist/services/incoming/incoming-tx-monitor-service.js.map +1 -1
  338. package/dist/services/incoming/incoming-tx-workers.d.ts +2 -2
  339. package/dist/services/incoming/incoming-tx-workers.d.ts.map +1 -1
  340. package/dist/services/incoming/incoming-tx-workers.js +1 -1
  341. package/dist/services/incoming/incoming-tx-workers.js.map +1 -1
  342. package/dist/services/incoming/safety-rules.d.ts.map +1 -1
  343. package/dist/services/incoming/safety-rules.js +3 -2
  344. package/dist/services/incoming/safety-rules.js.map +1 -1
  345. package/dist/services/incoming/subscription-multiplexer.d.ts +2 -6
  346. package/dist/services/incoming/subscription-multiplexer.d.ts.map +1 -1
  347. package/dist/services/incoming/subscription-multiplexer.js +1 -3
  348. package/dist/services/incoming/subscription-multiplexer.js.map +1 -1
  349. package/dist/services/monitoring/balance-monitor-service.d.ts.map +1 -1
  350. package/dist/services/monitoring/balance-monitor-service.js +2 -2
  351. package/dist/services/monitoring/balance-monitor-service.js.map +1 -1
  352. package/dist/services/signing-sdk/approval-channel-router.d.ts +7 -7
  353. package/dist/services/signing-sdk/approval-channel-router.d.ts.map +1 -1
  354. package/dist/services/signing-sdk/approval-channel-router.js +13 -13
  355. package/dist/services/signing-sdk/approval-channel-router.js.map +1 -1
  356. package/dist/services/signing-sdk/channels/index.d.ts +2 -2
  357. package/dist/services/signing-sdk/channels/index.d.ts.map +1 -1
  358. package/dist/services/signing-sdk/channels/index.js +1 -1
  359. package/dist/services/signing-sdk/channels/index.js.map +1 -1
  360. package/dist/services/signing-sdk/channels/push-relay-signing-channel.d.ts +59 -0
  361. package/dist/services/signing-sdk/channels/push-relay-signing-channel.d.ts.map +1 -0
  362. package/dist/services/signing-sdk/channels/push-relay-signing-channel.js +190 -0
  363. package/dist/services/signing-sdk/channels/push-relay-signing-channel.js.map +1 -0
  364. package/dist/services/signing-sdk/channels/telegram-signing-channel.d.ts +1 -1
  365. package/dist/services/signing-sdk/channels/telegram-signing-channel.js +1 -1
  366. package/dist/services/signing-sdk/channels/wallet-notification-channel.d.ts +6 -7
  367. package/dist/services/signing-sdk/channels/wallet-notification-channel.d.ts.map +1 -1
  368. package/dist/services/signing-sdk/channels/wallet-notification-channel.js +38 -55
  369. package/dist/services/signing-sdk/channels/wallet-notification-channel.js.map +1 -1
  370. package/dist/services/signing-sdk/index.d.ts +3 -3
  371. package/dist/services/signing-sdk/index.d.ts.map +1 -1
  372. package/dist/services/signing-sdk/index.js +2 -2
  373. package/dist/services/signing-sdk/index.js.map +1 -1
  374. package/dist/services/signing-sdk/preset-auto-setup.js +2 -2
  375. package/dist/services/signing-sdk/preset-auto-setup.js.map +1 -1
  376. package/dist/services/signing-sdk/sign-request-builder.d.ts +2 -2
  377. package/dist/services/signing-sdk/sign-request-builder.d.ts.map +1 -1
  378. package/dist/services/signing-sdk/sign-request-builder.js +17 -25
  379. package/dist/services/signing-sdk/sign-request-builder.js.map +1 -1
  380. package/dist/services/signing-sdk/wallet-app-service.d.ts +4 -0
  381. package/dist/services/signing-sdk/wallet-app-service.d.ts.map +1 -1
  382. package/dist/services/signing-sdk/wallet-app-service.js +12 -5
  383. package/dist/services/signing-sdk/wallet-app-service.js.map +1 -1
  384. package/dist/services/staking/aggregate-staking-balance.d.ts +24 -0
  385. package/dist/services/staking/aggregate-staking-balance.d.ts.map +1 -0
  386. package/dist/services/staking/aggregate-staking-balance.js +82 -0
  387. package/dist/services/staking/aggregate-staking-balance.js.map +1 -0
  388. package/dist/services/wc-session-service.d.ts.map +1 -1
  389. package/dist/services/wc-session-service.js +2 -1
  390. package/dist/services/wc-session-service.js.map +1 -1
  391. package/dist/services/wc-signing-bridge.js +2 -2
  392. package/dist/services/wc-signing-bridge.js.map +1 -1
  393. package/dist/services/x402/payment-signer.d.ts.map +1 -1
  394. package/dist/services/x402/payment-signer.js +2 -5
  395. package/dist/services/x402/payment-signer.js.map +1 -1
  396. package/dist/services/x402/ssrf-guard.d.ts +4 -23
  397. package/dist/services/x402/ssrf-guard.d.ts.map +1 -1
  398. package/dist/services/x402/ssrf-guard.js +3 -232
  399. package/dist/services/x402/ssrf-guard.js.map +1 -1
  400. package/dist/signing/capabilities/eip712-signer.d.ts.map +1 -1
  401. package/dist/signing/capabilities/eip712-signer.js +2 -0
  402. package/dist/signing/capabilities/eip712-signer.js.map +1 -1
  403. package/package.json +5 -5
  404. package/public/admin/assets/index-CpFF2lCo.js +3 -0
  405. package/public/admin/index.html +1 -1
  406. package/dist/notifications/channels/ntfy.d.ts +0 -13
  407. package/dist/notifications/channels/ntfy.d.ts.map +0 -1
  408. package/dist/notifications/channels/ntfy.js +0 -74
  409. package/dist/notifications/channels/ntfy.js.map +0 -1
  410. package/dist/services/signing-sdk/channels/ntfy-signing-channel.d.ts +0 -66
  411. package/dist/services/signing-sdk/channels/ntfy-signing-channel.d.ts.map +0 -1
  412. package/dist/services/signing-sdk/channels/ntfy-signing-channel.js +0 -270
  413. package/dist/services/signing-sdk/channels/ntfy-signing-channel.js.map +0 -1
  414. package/public/admin/assets/index-CQ3i4P2U.js +0 -3
@@ -0,0 +1,1527 @@
1
+ /**
2
+ * daemon-startup.ts - Extracted 6-step startup sequence from DaemonLifecycle.
3
+ *
4
+ * Contains the full _startInternal() body that was previously ~1,650 lines
5
+ * in daemon.ts. Receives a DaemonState context object to read/write daemon fields.
6
+ *
7
+ * @see docs/28-daemon-lifecycle-cli.md
8
+ */
9
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { createRequire } from 'node:module';
11
+ import { join, dirname } from 'node:path';
12
+ import { WAIaaSError, BUILT_IN_RPC_DEFAULTS, RpcPool, safeJsonParse } from '@waiaas/core';
13
+ import { z } from 'zod';
14
+ import { KillSwitchService } from '../services/kill-switch-service.js';
15
+ import { AutoStopService } from '../services/autostop-service.js';
16
+ import { InMemoryCounter } from '../infrastructure/metrics/in-memory-counter.js';
17
+ import { AdminStatsService } from '../services/admin-stats-service.js';
18
+ import { createDatabase, pushSchema, checkSchemaCompatibility } from '../infrastructure/database/index.js';
19
+ import { loadConfig } from '../infrastructure/config/index.js';
20
+ import { keyValueStore } from '../infrastructure/database/schema.js';
21
+ import { eq } from 'drizzle-orm';
22
+ import { decrypt } from '../infrastructure/keystore/crypto.js';
23
+ import { DelayQueue } from '../workflow/delay-queue.js';
24
+ import { ApprovalWorkflow } from '../workflow/approval-workflow.js';
25
+ import { DatabasePolicyEngine } from '../pipeline/database-policy-engine.js';
26
+ import { JwtSecretManager } from '../infrastructure/jwt/index.js';
27
+ import argon2 from 'argon2';
28
+ import { BackgroundWorkers } from './workers.js';
29
+ import { resolveRpcUrl, resolveRpcUrlFromPool } from '../infrastructure/adapter-pool.js';
30
+ import { withTimeout } from './daemon.js';
31
+ const esmRequire = createRequire(import.meta.url);
32
+ /**
33
+ * 6-step startup sequence with per-step timeouts.
34
+ * Receives DaemonState to read/write daemon fields.
35
+ */
36
+ export async function startDaemon(state, dataDir, masterPassword) {
37
+ // Store master password for route handlers
38
+ state.masterPassword = masterPassword;
39
+ // ------------------------------------------------------------------
40
+ // Step 1: Environment validation + config + flock (5s, fail-fast)
41
+ // ------------------------------------------------------------------
42
+ await withTimeout((async () => {
43
+ // Ensure data directory exists
44
+ if (!existsSync(dataDir)) {
45
+ mkdirSync(dataDir, { recursive: true });
46
+ }
47
+ // Load config
48
+ state._config = loadConfig(dataDir);
49
+ // Acquire daemon lock (flock-like via proper-lockfile)
50
+ await state.acquireDaemonLock(dataDir);
51
+ console.debug('Step 1: Config loaded, daemon lock acquired');
52
+ })(), 5_000, 'STEP1_CONFIG_LOCK');
53
+ // ------------------------------------------------------------------
54
+ // Step 2: Database initialization (30s, fail-fast)
55
+ // ------------------------------------------------------------------
56
+ await withTimeout((async () => {
57
+ const dbPath = join(dataDir, state._config.database.path);
58
+ // Ensure DB directory exists
59
+ const dbDir = dirname(dbPath);
60
+ if (!existsSync(dbDir)) {
61
+ mkdirSync(dbDir, { recursive: true });
62
+ }
63
+ const { sqlite, db } = createDatabase(dbPath);
64
+ state.sqlite = sqlite;
65
+ state._db = db;
66
+ // Check schema compatibility before migration
67
+ const compatibility = checkSchemaCompatibility(sqlite);
68
+ if (compatibility.action === 'reject') {
69
+ console.error(`Step 2: Schema incompatible -- ${compatibility.message}`);
70
+ throw new WAIaaSError('SCHEMA_INCOMPATIBLE', {
71
+ message: compatibility.message,
72
+ });
73
+ }
74
+ if (compatibility.action === 'migrate') {
75
+ console.debug('Step 2: Schema migration needed, applying...');
76
+ }
77
+ // Create all tables + run migrations (idempotent)
78
+ pushSchema(sqlite);
79
+ // Auto-import config.toml operational settings into DB (first boot only)
80
+ const { SettingsService } = await import('../infrastructure/settings/index.js');
81
+ state._settingsService = new SettingsService({
82
+ db: state._db,
83
+ config: state._config,
84
+ masterPassword,
85
+ passwordRef: state.passwordRef ?? undefined,
86
+ });
87
+ const importResult = state._settingsService.importFromConfig();
88
+ if (importResult.imported > 0) {
89
+ console.debug(`Step 2: Settings imported from config.toml (${importResult.imported} keys)`);
90
+ }
91
+ console.debug('Step 2: Database initialized');
92
+ })(), 30_000, 'STEP2_DATABASE');
93
+ // ------------------------------------------------------------------
94
+ // Step 2b: Master password validation (fail-fast)
95
+ // ------------------------------------------------------------------
96
+ await withTimeout((async () => {
97
+ const existingHash = state._db
98
+ .select()
99
+ .from(keyValueStore)
100
+ .where(eq(keyValueStore.key, 'master_password_hash'))
101
+ .get();
102
+ if (existingHash) {
103
+ // Path A: DB hash exists -> verify against stored hash
104
+ const isValid = await argon2.verify(existingHash.value, masterPassword);
105
+ if (!isValid) {
106
+ console.error('Invalid master password.');
107
+ process.exit(1);
108
+ }
109
+ console.debug('Step 2b: Master password verified (DB hash)');
110
+ }
111
+ else {
112
+ // Path B: No DB hash -> check for existing keystore files
113
+ const keystoreDir = join(dataDir, 'keystore');
114
+ const keystoreFiles = existsSync(keystoreDir)
115
+ ? readdirSync(keystoreDir).filter(f => f.endsWith('.json'))
116
+ : [];
117
+ if (keystoreFiles.length > 0) {
118
+ // Existing user migration: validate by decrypting first keystore
119
+ const keystorePath = join(keystoreDir, keystoreFiles[0]);
120
+ const content = readFileSync(keystorePath, 'utf-8');
121
+ const KeystoreSchema = z.object({
122
+ crypto: z.object({
123
+ cipherparams: z.object({ iv: z.string() }),
124
+ ciphertext: z.string(),
125
+ authTag: z.string(),
126
+ kdfparams: z.object({
127
+ salt: z.string(),
128
+ memoryCost: z.number(),
129
+ timeCost: z.number(),
130
+ parallelism: z.number(),
131
+ hashLength: z.number(),
132
+ }),
133
+ }),
134
+ });
135
+ const parseResult = safeJsonParse(content, KeystoreSchema);
136
+ if (!parseResult.success) {
137
+ console.error('Invalid keystore file format:', parseResult.error.message);
138
+ process.exit(1);
139
+ }
140
+ const parsed = parseResult.data;
141
+ const encrypted = {
142
+ iv: Buffer.from(parsed.crypto.cipherparams.iv, 'hex'),
143
+ ciphertext: Buffer.from(parsed.crypto.ciphertext, 'hex'),
144
+ authTag: Buffer.from(parsed.crypto.authTag, 'hex'),
145
+ salt: Buffer.from(parsed.crypto.kdfparams.salt, 'hex'),
146
+ kdfparams: parsed.crypto.kdfparams,
147
+ };
148
+ try {
149
+ const plain = await decrypt(encrypted, masterPassword);
150
+ plain.fill(0); // zero immediately
151
+ }
152
+ catch {
153
+ console.error('Invalid master password. Cannot decrypt existing wallets.');
154
+ process.exit(1);
155
+ }
156
+ console.debug('Step 2b: Master password verified (keystore migration)');
157
+ }
158
+ else {
159
+ console.debug('Step 2b: First install, no password validation needed');
160
+ }
161
+ // Store hash in DB for future startups
162
+ const hash = await argon2.hash(masterPassword, {
163
+ type: argon2.argon2id,
164
+ memoryCost: 19456,
165
+ timeCost: 2,
166
+ parallelism: 1,
167
+ });
168
+ state._db
169
+ .insert(keyValueStore)
170
+ .values({
171
+ key: 'master_password_hash',
172
+ value: hash,
173
+ updatedAt: new Date(),
174
+ })
175
+ .onConflictDoNothing()
176
+ .run();
177
+ }
178
+ })(), 30_000, 'STEP2B_PASSWORD_VALIDATION');
179
+ // ------------------------------------------------------------------
180
+ // Step 3: Keystore unlock (30s, fail-fast)
181
+ // ------------------------------------------------------------------
182
+ await withTimeout((async () => {
183
+ // Dynamic import to avoid circular dependency issues
184
+ const { LocalKeyStore: KeyStoreCls } = await import('../infrastructure/keystore/index.js');
185
+ const keystoreDir = join(dataDir, 'keystore');
186
+ if (!existsSync(keystoreDir)) {
187
+ mkdirSync(keystoreDir, { recursive: true });
188
+ }
189
+ state.keyStore = new KeyStoreCls(keystoreDir);
190
+ // v1.1: just verify keystore infrastructure is accessible
191
+ // Full key decryption happens when agents are accessed
192
+ if (masterPassword) {
193
+ console.debug('Step 3: Keystore infrastructure verified (master password provided)');
194
+ }
195
+ else {
196
+ console.debug('Step 3: Keystore infrastructure verified (no master password)');
197
+ }
198
+ })(), 30_000, 'STEP3_KEYSTORE');
199
+ // ------------------------------------------------------------------
200
+ // Step 4: Adapter pool initialization (10s, fail-soft)
201
+ // ------------------------------------------------------------------
202
+ try {
203
+ await withTimeout((async () => {
204
+ const { AdapterPool, configKeyToNetwork: configKeyToNet } = await import('../infrastructure/adapter-pool.js');
205
+ // 1. Create empty RpcPool with onEvent callback for notifications
206
+ state.rpcPool = new RpcPool({
207
+ onEvent: (event) => {
208
+ // RPC pool health notifications -- use 'system' as walletId
209
+ // since these are infrastructure-level alerts, not wallet-specific.
210
+ if (state.notificationService) {
211
+ const vars = {
212
+ network: event.network,
213
+ url: event.url,
214
+ errorCount: String(event.failureCount),
215
+ totalEndpoints: String(event.totalEndpoints),
216
+ };
217
+ void state.notificationService.notify(event.type, 'system', vars);
218
+ }
219
+ },
220
+ });
221
+ // 2. Seed config.toml URLs first (highest priority)
222
+ // WAIAAS_RPC_* env vars are already applied to config.rpc by applyEnvOverrides in loader.ts
223
+ const rpcConfig = state._config.rpc;
224
+ for (const [configKey, url] of Object.entries(rpcConfig)) {
225
+ if (typeof url !== 'string' || !url)
226
+ continue;
227
+ const network = configKeyToNet(configKey);
228
+ if (network) {
229
+ state.rpcPool.register(network, [url]);
230
+ }
231
+ }
232
+ // 3. Register built-in defaults (lower priority, appended after config URLs)
233
+ for (const [network, urls] of Object.entries(BUILT_IN_RPC_DEFAULTS)) {
234
+ state.rpcPool.register(network, [...urls]);
235
+ }
236
+ // 4. Create AdapterPool with RpcPool
237
+ state.adapterPool = new AdapterPool(state.rpcPool);
238
+ console.debug(`Step 4: AdapterPool created with RpcPool (${state.rpcPool.getNetworks().length} networks seeded)`);
239
+ })(), 10_000, 'STEP4_ADAPTER');
240
+ }
241
+ catch (err) {
242
+ // fail-soft: log warning but continue (daemon runs without chain adapter)
243
+ console.warn('Step 4 (fail-soft): AdapterPool init warning:', err);
244
+ state.adapterPool = null;
245
+ state.rpcPool = null;
246
+ }
247
+ // ------------------------------------------------------------------
248
+ // Step 4b: Create workflow instances (DelayQueue + ApprovalWorkflow)
249
+ // ------------------------------------------------------------------
250
+ if (state._db && state.sqlite && state._config) {
251
+ state.delayQueue = new DelayQueue({ db: state._db, sqlite: state.sqlite });
252
+ state.approvalWorkflow = new ApprovalWorkflow({
253
+ db: state._db,
254
+ sqlite: state.sqlite,
255
+ config: {
256
+ policy_defaults_approval_timeout: state._config.security.policy_defaults_approval_timeout,
257
+ },
258
+ onApproved: (txId) => state.handleApprovalApproved(txId),
259
+ });
260
+ console.debug('Step 4b: Workflow instances created (DelayQueue + ApprovalWorkflow)');
261
+ }
262
+ // ------------------------------------------------------------------
263
+ // Step 4c: JWT Secret Manager + master password hash
264
+ // ------------------------------------------------------------------
265
+ if (state._db) {
266
+ state.jwtSecretManager = new JwtSecretManager(state._db);
267
+ await state.jwtSecretManager.initialize();
268
+ state.masterPasswordHash = await argon2.hash(masterPassword, {
269
+ type: argon2.argon2id,
270
+ memoryCost: 19456,
271
+ timeCost: 2,
272
+ parallelism: 1,
273
+ });
274
+ // Create mutable ref for live password/hash updates (password change API)
275
+ state.passwordRef = { password: masterPassword, hash: state.masterPasswordHash };
276
+ console.debug('Step 4c: JWT secret manager initialized, master password hashed');
277
+ }
278
+ // ------------------------------------------------------------------
279
+ // Step 4c-2: KillSwitchService initialization
280
+ // ------------------------------------------------------------------
281
+ if (state.sqlite) {
282
+ state.killSwitchService = new KillSwitchService({
283
+ sqlite: state.sqlite,
284
+ // notificationService will be set after Step 4d
285
+ eventBus: state.eventBus,
286
+ });
287
+ state.killSwitchService.ensureInitialized();
288
+ console.debug('Step 4c-2: KillSwitchService initialized');
289
+ }
290
+ // ------------------------------------------------------------------
291
+ // Step 4d: Notification Service initialization (fail-soft)
292
+ // ------------------------------------------------------------------
293
+ try {
294
+ // Always create NotificationService regardless of config.toml enabled value.
295
+ // When enabled=false, service starts with 0 channels (no notifications sent).
296
+ // Admin UI can dynamically enable via hot-reload at runtime.
297
+ const { NotificationService, TelegramChannel, DiscordChannel, SlackChannel } = await import('../notifications/index.js');
298
+ // Read notification settings from SettingsService first, fall back to config.toml
299
+ const ss = state._settingsService;
300
+ const notifLocale = ((ss ? ss.get('notifications.locale') : null)
301
+ || state._config.notifications.locale || 'en');
302
+ const notifRateLimitRpm = Number((ss ? ss.get('notifications.rate_limit_rpm') : null)
303
+ || state._config.notifications.rate_limit_rpm
304
+ || 20);
305
+ state.notificationService = new NotificationService({
306
+ db: state._db ?? undefined,
307
+ config: {
308
+ locale: notifLocale,
309
+ rateLimitRpm: notifRateLimitRpm,
310
+ },
311
+ });
312
+ // Inject SettingsService for category filtering
313
+ if (ss) {
314
+ state.notificationService.setSettingsService(ss);
315
+ }
316
+ // Initialize configured channels: SettingsService (DB) takes priority over config.toml
317
+ const notifEnabled = ss
318
+ ? ss.get('notifications.enabled') === 'true'
319
+ : state._config.notifications.enabled;
320
+ if (notifEnabled) {
321
+ const notifConfig = state._config.notifications;
322
+ const tgToken = (ss ? ss.get('notifications.telegram_bot_token') : null)
323
+ || notifConfig.telegram_bot_token;
324
+ const tgChatId = (ss ? ss.get('notifications.telegram_chat_id') : null)
325
+ || notifConfig.telegram_chat_id;
326
+ if (tgToken && tgChatId) {
327
+ const telegram = new TelegramChannel();
328
+ await telegram.initialize({
329
+ telegram_bot_token: tgToken,
330
+ telegram_chat_id: tgChatId,
331
+ });
332
+ state.notificationService.addChannel(telegram);
333
+ }
334
+ const discordUrl = (ss ? ss.get('notifications.discord_webhook_url') : null)
335
+ || notifConfig.discord_webhook_url;
336
+ if (discordUrl) {
337
+ const discord = new DiscordChannel();
338
+ await discord.initialize({
339
+ discord_webhook_url: discordUrl,
340
+ });
341
+ state.notificationService.addChannel(discord);
342
+ }
343
+ // Global NtfyChannel removed in v29.10 -- per-wallet topics now in wallet_apps table.
344
+ // Per-wallet ntfy channels are managed by the signing SDK / notification routing layer.
345
+ const slackUrl = (ss ? ss.get('notifications.slack_webhook_url') : null)
346
+ || notifConfig.slack_webhook_url;
347
+ if (slackUrl) {
348
+ const slack = new SlackChannel();
349
+ await slack.initialize({
350
+ slack_webhook_url: slackUrl,
351
+ });
352
+ state.notificationService.addChannel(slack);
353
+ }
354
+ }
355
+ const channelNames = state.notificationService.getChannelNames();
356
+ console.debug(`Step 4d: NotificationService initialized (${channelNames.length} channels: ${channelNames.join(', ') || 'none'})`);
357
+ }
358
+ catch (err) {
359
+ console.warn('Step 4d (fail-soft): NotificationService init warning:', err);
360
+ state.notificationService = null;
361
+ }
362
+ // Wire NotificationService to KillSwitchService (created before Step 4d)
363
+ if (state.killSwitchService && state.notificationService) {
364
+ // Re-create with notification service attached
365
+ state.killSwitchService = new KillSwitchService({
366
+ sqlite: state.sqlite,
367
+ notificationService: state.notificationService,
368
+ eventBus: state.eventBus,
369
+ });
370
+ state.killSwitchService.ensureInitialized();
371
+ }
372
+ // ------------------------------------------------------------------
373
+ // Step 4c-3: AutoStop Engine (fail-soft)
374
+ // ------------------------------------------------------------------
375
+ try {
376
+ if (state.sqlite && state.killSwitchService && state._settingsService) {
377
+ const autoStopConfig = {
378
+ consecutiveFailuresThreshold: parseInt(state._settingsService.get('autostop.consecutive_failures_threshold'), 10),
379
+ unusualActivityThreshold: parseInt(state._settingsService.get('autostop.unusual_activity_threshold'), 10),
380
+ unusualActivityWindowSec: parseInt(state._settingsService.get('autostop.unusual_activity_window_sec'), 10),
381
+ idleTimeoutSec: parseInt(state._settingsService.get('autostop.idle_timeout_sec'), 10),
382
+ idleCheckIntervalSec: parseInt(state._settingsService.get('autostop.idle_check_interval_sec'), 10),
383
+ enabled: state._settingsService.get('autostop.enabled') === 'true',
384
+ };
385
+ state.autoStopService = new AutoStopService({
386
+ sqlite: state.sqlite,
387
+ eventBus: state.eventBus,
388
+ killSwitchService: state.killSwitchService,
389
+ notificationService: state.notificationService ?? undefined,
390
+ config: autoStopConfig,
391
+ });
392
+ if (autoStopConfig.enabled) {
393
+ state.autoStopService.start();
394
+ console.debug('Step 4c-3: AutoStop engine started');
395
+ }
396
+ else {
397
+ console.debug('Step 4c-3: AutoStop engine disabled');
398
+ }
399
+ }
400
+ }
401
+ catch (err) {
402
+ console.warn('Step 4c-3 (fail-soft): AutoStop engine init warning:', err);
403
+ state.autoStopService = null;
404
+ }
405
+ // ------------------------------------------------------------------
406
+ // Step 4c-3b: InMemoryCounter + AdminStatsService (fail-soft)
407
+ // ------------------------------------------------------------------
408
+ try {
409
+ if (state.sqlite) {
410
+ state.metricsCounter = new InMemoryCounter();
411
+ // v30.2: inject metricsCounter into AutoStopService (STAT-02)
412
+ if (state.autoStopService) {
413
+ state.autoStopService.setMetricsCounter(state.metricsCounter);
414
+ }
415
+ const { version: daemonVersion } = esmRequire('../../package.json');
416
+ state.adminStatsService = new AdminStatsService({
417
+ sqlite: state.sqlite,
418
+ metricsCounter: state.metricsCounter,
419
+ autoStopService: state.autoStopService ?? undefined,
420
+ startTime: state.daemonStartTime,
421
+ version: daemonVersion,
422
+ dataDir,
423
+ });
424
+ console.debug('Step 4c-3b: AdminStatsService created');
425
+ }
426
+ }
427
+ catch (err) {
428
+ console.warn('Step 4c-3b (fail-soft): AdminStatsService init warning:', err);
429
+ state.adminStatsService = null;
430
+ }
431
+ // ------------------------------------------------------------------
432
+ // Step 4c-4: BalanceMonitorService initialization (fail-soft)
433
+ // ------------------------------------------------------------------
434
+ try {
435
+ if (state.sqlite && state.adapterPool && state._config && state._settingsService) {
436
+ const { BalanceMonitorService: BalanceMonitorCls } = await import('../services/monitoring/balance-monitor-service.js');
437
+ const monitorConfig = {
438
+ checkIntervalSec: parseInt(state._settingsService.get('monitoring.check_interval_sec'), 10),
439
+ lowBalanceThresholdSol: parseFloat(state._settingsService.get('monitoring.low_balance_threshold_sol')),
440
+ lowBalanceThresholdEth: parseFloat(state._settingsService.get('monitoring.low_balance_threshold_eth')),
441
+ cooldownHours: parseInt(state._settingsService.get('monitoring.cooldown_hours'), 10),
442
+ enabled: state._settingsService.get('monitoring.enabled') === 'true',
443
+ };
444
+ state.balanceMonitorService = new BalanceMonitorCls({
445
+ sqlite: state.sqlite,
446
+ adapterPool: state.adapterPool,
447
+ config: state._config,
448
+ notificationService: state.notificationService ?? undefined,
449
+ monitorConfig,
450
+ });
451
+ if (monitorConfig.enabled) {
452
+ state.balanceMonitorService.start();
453
+ console.debug('Step 4c-4: Balance monitor started');
454
+ }
455
+ else {
456
+ console.debug('Step 4c-4: Balance monitor disabled');
457
+ }
458
+ }
459
+ }
460
+ catch (err) {
461
+ console.warn('Step 4c-4 (fail-soft): Balance monitor init warning:', err);
462
+ state.balanceMonitorService = null;
463
+ }
464
+ // ------------------------------------------------------------------
465
+ // Step 4c-5: TelegramBotService initialization (fail-soft)
466
+ // ------------------------------------------------------------------
467
+ try {
468
+ // Read telegram settings from SettingsService (falls back to config.toml)
469
+ const ss = state._settingsService;
470
+ // Token priority: telegram.bot_token > notifications.telegram_bot_token > config.toml
471
+ const botToken = (ss ? (ss.get('telegram.bot_token') || ss.get('notifications.telegram_bot_token')) : null)
472
+ || state._config.telegram.bot_token;
473
+ if (botToken) {
474
+ const { TelegramBotService, TelegramApi } = await import('../infrastructure/telegram/index.js');
475
+ const telegramApi = new TelegramApi(botToken);
476
+ const telegramLocale = ((ss ? ss.get('telegram.locale') : null)
477
+ || state._config.telegram.locale
478
+ || state._config.notifications.locale
479
+ || 'en');
480
+ state.telegramBotService = new TelegramBotService({
481
+ sqlite: state.sqlite,
482
+ api: telegramApi,
483
+ locale: telegramLocale,
484
+ killSwitchService: state.killSwitchService ?? undefined,
485
+ notificationService: state.notificationService ?? undefined,
486
+ settingsService: state._settingsService ?? undefined,
487
+ onApproved: (txId) => state.handleApprovalApproved(txId),
488
+ });
489
+ state.telegramBotService.start();
490
+ state.telegramBotRef.current = state.telegramBotService;
491
+ console.debug('Step 4c-5: Telegram Bot started');
492
+ }
493
+ else {
494
+ console.debug('Step 4c-5: Telegram Bot disabled');
495
+ }
496
+ }
497
+ catch (err) {
498
+ console.warn('Step 4c-5 (fail-soft): Telegram Bot init warning:', err);
499
+ state.telegramBotService = null;
500
+ state.telegramBotRef.current = null;
501
+ }
502
+ // ------------------------------------------------------------------
503
+ // Step 4c-6: WalletConnect service initialization (fail-soft)
504
+ // ------------------------------------------------------------------
505
+ try {
506
+ const wcProjectId = state._settingsService?.get('walletconnect.project_id');
507
+ if (wcProjectId) {
508
+ const { WcSessionService } = await import('../services/wc-session-service.js');
509
+ state.wcSessionService = new WcSessionService({
510
+ sqlite: state.sqlite,
511
+ settingsService: state._settingsService,
512
+ });
513
+ await state.wcSessionService.initialize();
514
+ state.wcServiceRef.current = state.wcSessionService;
515
+ console.debug('Step 4c-6: WalletConnect service initialized');
516
+ }
517
+ else {
518
+ console.debug('Step 4c-6: WalletConnect disabled (no project_id)');
519
+ }
520
+ }
521
+ catch (err) {
522
+ console.warn('Step 4c-6 (fail-soft): WalletConnect init warning:', err);
523
+ state.wcSessionService = null;
524
+ state.wcServiceRef.current = null;
525
+ }
526
+ // ------------------------------------------------------------------
527
+ // Step 4c-7: WcSigningBridge (fail-soft, requires WcSessionService + ApprovalWorkflow)
528
+ // ------------------------------------------------------------------
529
+ try {
530
+ if (state.wcSessionService && state.approvalWorkflow && state.sqlite) {
531
+ const { WcSigningBridge } = await import('../services/wc-signing-bridge.js');
532
+ state.wcSigningBridgeRef.current = new WcSigningBridge({
533
+ wcServiceRef: state.wcServiceRef,
534
+ approvalWorkflow: state.approvalWorkflow,
535
+ sqlite: state.sqlite,
536
+ notificationService: state.notificationService ?? undefined,
537
+ eventBus: state.eventBus,
538
+ });
539
+ console.debug('Step 4c-7: WcSigningBridge initialized');
540
+ }
541
+ }
542
+ catch (err) {
543
+ console.warn('Step 4c-7 (fail-soft): WcSigningBridge init warning:', err);
544
+ state.wcSigningBridgeRef.current = null;
545
+ }
546
+ // ------------------------------------------------------------------
547
+ // Step 4c-8: Signing SDK lifecycle (fail-soft)
548
+ // ------------------------------------------------------------------
549
+ try {
550
+ if (state._settingsService?.get('signing_sdk.enabled') === 'true') {
551
+ const { SignRequestBuilder, SignResponseHandler, WalletLinkRegistry, PushRelaySigningChannel, TelegramSigningChannel, ApprovalChannelRouter, WalletNotificationChannel, } = await import('../services/signing-sdk/index.js');
552
+ const walletLinkRegistry = new WalletLinkRegistry(state._settingsService);
553
+ const signRequestBuilder = new SignRequestBuilder({
554
+ settingsService: state._settingsService,
555
+ walletLinkRegistry,
556
+ sqlite: state.sqlite, // per-wallet topic lookup from wallet_apps table
557
+ });
558
+ const signResponseHandler = new SignResponseHandler({ sqlite: state.sqlite }, { onApproved: (txId) => state.handleApprovalApproved(txId) });
559
+ const pushRelayChannel = new PushRelaySigningChannel({
560
+ signRequestBuilder,
561
+ signResponseHandler,
562
+ settingsService: state._settingsService,
563
+ });
564
+ // Conditionally create TelegramSigningChannel (only if Telegram bot is running)
565
+ let telegramChannel;
566
+ if (state.telegramBotService) {
567
+ const { TelegramApi } = await import('../infrastructure/telegram/index.js');
568
+ const botToken = (state._settingsService
569
+ ? state._settingsService.get('telegram.bot_token') ||
570
+ state._settingsService.get('notifications.telegram_bot_token')
571
+ : null) || state._config.telegram.bot_token;
572
+ if (botToken) {
573
+ const signingTelegramApi = new TelegramApi(botToken);
574
+ telegramChannel = new TelegramSigningChannel({
575
+ signRequestBuilder,
576
+ signResponseHandler,
577
+ settingsService: state._settingsService,
578
+ telegramApi: signingTelegramApi,
579
+ });
580
+ }
581
+ }
582
+ state.approvalChannelRouter = new ApprovalChannelRouter({
583
+ sqlite: state.sqlite,
584
+ settingsService: state._settingsService,
585
+ pushRelayChannel,
586
+ telegramChannel,
587
+ });
588
+ // Inject signResponseHandler into TelegramBotService for /sign_response command (GAP-2: CHAN-04)
589
+ if (state.telegramBotService) {
590
+ state.telegramBotService.setSignResponseHandler(signResponseHandler);
591
+ console.debug('Step 4c-8: signResponseHandler injected into TelegramBotService');
592
+ }
593
+ // Wallet Notification Side Channel (v2.7)
594
+ const walletNotifChannel = new WalletNotificationChannel({
595
+ sqlite: state.sqlite,
596
+ settingsService: state._settingsService,
597
+ });
598
+ state.notificationService?.setWalletNotificationChannel(walletNotifChannel);
599
+ console.debug('Step 4c-8: WalletNotificationChannel injected into NotificationService');
600
+ console.debug('Step 4c-8: Signing SDK initialized (ApprovalChannelRouter + channels)');
601
+ }
602
+ else {
603
+ console.debug('Step 4c-8: Signing SDK disabled');
604
+ }
605
+ }
606
+ catch (err) {
607
+ console.warn('Step 4c-8 (fail-soft): Signing SDK init warning:', err);
608
+ }
609
+ // ------------------------------------------------------------------
610
+ // Step 4c-9: IncomingTxMonitorService initialization (fail-soft)
611
+ // ------------------------------------------------------------------
612
+ // Pre-create BackgroundWorkers so Step 4c-9 (incoming monitor) can register its workers.
613
+ // startAll() is still called in Step 6 after all workers are registered.
614
+ if (!state.workers) {
615
+ state.workers = new BackgroundWorkers();
616
+ }
617
+ try {
618
+ if (state.sqlite && state._settingsService) {
619
+ const incoming_enabled = state._settingsService.get('incoming.enabled');
620
+ if (incoming_enabled === 'true') {
621
+ const { IncomingTxMonitorService: IncomingTxMonitorCls } = await import('../services/incoming/incoming-tx-monitor-service.js');
622
+ // Build config from SettingsService
623
+ const ss = state._settingsService;
624
+ const monitorConfig = {
625
+ enabled: true,
626
+ pollIntervalSec: parseInt(ss.get('incoming.poll_interval') || '30', 10),
627
+ retentionDays: parseInt(ss.get('incoming.retention_days') || '90', 10),
628
+ dustThresholdUsd: parseFloat(ss.get('incoming.suspicious_dust_usd') || '0.01'),
629
+ amountMultiplier: parseFloat(ss.get('incoming.suspicious_amount_multiplier') || '10'),
630
+ cooldownMinutes: parseInt(ss.get('incoming.cooldown_minutes') || '5', 10),
631
+ };
632
+ // subscriberFactory creates chain-specific subscribers via dynamic import
633
+ // URL resolution: prefer RpcPool (multi-endpoint rotation), fallback to SettingsService
634
+ const subscriberFactory = async (chain, network) => {
635
+ const sSvc = state._settingsService;
636
+ // Per-network WSS URL resolution (#193):
637
+ // Priority: per-network key -> global incoming.wss_url -> auto-derive from RPC URL
638
+ const resolveWssUrl = (net, rpcUrl) => {
639
+ const perNetwork = sSvc.get(`incoming.wss_url.${net}`);
640
+ if (perNetwork)
641
+ return perNetwork;
642
+ const global = sSvc.get('incoming.wss_url');
643
+ if (global)
644
+ return global;
645
+ return rpcUrl.replace(/^https:\/\//, 'wss://');
646
+ };
647
+ if (chain === 'solana') {
648
+ const rpcUrl = resolveRpcUrlFromPool(state.rpcPool, sSvc.get.bind(sSvc), chain, network);
649
+ const wssUrl = resolveWssUrl(network, rpcUrl);
650
+ const { SolanaIncomingSubscriber } = await import('@waiaas/adapter-solana');
651
+ return new SolanaIncomingSubscriber({ rpcUrl, wsUrl: wssUrl });
652
+ }
653
+ // EVM chains -- dynamic URL resolution via RPC Pool (#199)
654
+ const rpcPool = state.rpcPool;
655
+ const resolveRpcUrlFn = () => resolveRpcUrlFromPool(rpcPool, sSvc.get.bind(sSvc), chain, network);
656
+ const initialRpcUrl = resolveRpcUrlFn();
657
+ const wssUrl = resolveWssUrl(network, initialRpcUrl);
658
+ const { EvmIncomingSubscriber } = await import('@waiaas/adapter-evm');
659
+ const ns = state.notificationService;
660
+ // Token address resolver for getLogs address filter (#203)
661
+ const { TokenRegistryService } = await import('../infrastructure/token-registry/index.js');
662
+ const tokenRegistry = state._db ? new TokenRegistryService(state._db) : null;
663
+ let cachedTokenAddresses = [];
664
+ let cacheExpiry = 0;
665
+ const resolveTokenAddresses = () => {
666
+ const now = Date.now();
667
+ if (now < cacheExpiry)
668
+ return cachedTokenAddresses;
669
+ // Refresh every 60s to pick up runtime token additions
670
+ try {
671
+ if (tokenRegistry) {
672
+ // Synchronous access: getTokensForNetwork is async but uses sync DB
673
+ // Use a cached snapshot refreshed periodically
674
+ void tokenRegistry.getTokensForNetwork(network).then((tokens) => {
675
+ cachedTokenAddresses = tokens
676
+ .map((t) => t.address);
677
+ cacheExpiry = Date.now() + 60_000;
678
+ });
679
+ }
680
+ }
681
+ catch { /* keep previous cache */ }
682
+ return cachedTokenAddresses;
683
+ };
684
+ // Prime the cache immediately
685
+ if (tokenRegistry) {
686
+ try {
687
+ const tokens = await tokenRegistry.getTokensForNetwork(network);
688
+ cachedTokenAddresses = tokens.map((t) => t.address);
689
+ cacheExpiry = Date.now() + 60_000;
690
+ }
691
+ catch { /* empty cache is fine -- ERC-20 polling will be skipped */ }
692
+ }
693
+ return new EvmIncomingSubscriber({
694
+ resolveRpcUrl: resolveRpcUrlFn,
695
+ reportRpcFailure: (url) => rpcPool?.reportFailure(network, url),
696
+ reportRpcSuccess: (url) => rpcPool?.reportSuccess(network, url),
697
+ wsUrl: wssUrl !== initialRpcUrl.replace(/^https:\/\//, 'wss://') ? wssUrl : undefined,
698
+ resolveTokenAddresses,
699
+ onRpcAlert: ns ? (alert) => {
700
+ ns.notify(alert.type, alert.walletId, {
701
+ network: alert.network,
702
+ errorCount: String(alert.errorCount),
703
+ lastError: alert.lastError,
704
+ ...(alert.fromBlock ? { fromBlock: alert.fromBlock } : {}),
705
+ ...(alert.toBlock ? { toBlock: alert.toBlock } : {}),
706
+ });
707
+ } : undefined,
708
+ });
709
+ };
710
+ state.incomingTxMonitorService = new IncomingTxMonitorCls({
711
+ sqlite: state.sqlite,
712
+ db: state._db,
713
+ workers: state.workers ?? new BackgroundWorkers(),
714
+ eventBus: state.eventBus,
715
+ killSwitchService: state.killSwitchService,
716
+ notificationService: state.notificationService,
717
+ subscriberFactory,
718
+ config: monitorConfig,
719
+ });
720
+ await state.incomingTxMonitorService.start();
721
+ console.debug('Step 4c-9: Incoming TX monitor started');
722
+ }
723
+ else {
724
+ console.debug('Step 4c-9: Incoming TX monitor disabled');
725
+ }
726
+ }
727
+ }
728
+ catch (err) {
729
+ console.warn('Step 4c-9 (fail-soft): Incoming TX monitor init warning:', err);
730
+ state.incomingTxMonitorService = null;
731
+ }
732
+ // ------------------------------------------------------------------
733
+ // Step 4c-10: AsyncPollingService initialization (fail-soft)
734
+ // ------------------------------------------------------------------
735
+ try {
736
+ if (state._db) {
737
+ const { AsyncPollingService } = await import('../services/async-polling-service.js');
738
+ const { transactions: txTable } = await import('../infrastructure/database/schema.js');
739
+ state._asyncPollingService = new AsyncPollingService(state._db, {
740
+ emitNotification: (eventType, walletId, data) => {
741
+ if (state.notificationService) {
742
+ void state.notificationService.notify(eventType, walletId, undefined, // vars (template interpolation -- not needed for bridge events)
743
+ data);
744
+ }
745
+ },
746
+ releaseReservation: (txId) => {
747
+ // Reset reserved_amount and reserved_amount_usd to 0 for the transaction
748
+ state._db
749
+ .update(txTable)
750
+ .set({ reservedAmount: '0', reservedAmountUsd: null })
751
+ .where(eq(txTable.id, txId))
752
+ .run();
753
+ },
754
+ resumePipeline: (txId, walletId) => {
755
+ // Gas condition met: re-enter pipeline at stage 4 (execute from stage 4 onward)
756
+ void state.executeFromStage4(txId, walletId);
757
+ },
758
+ });
759
+ console.debug('Step 4c-10: AsyncPollingService initialized (with callbacks)');
760
+ }
761
+ }
762
+ catch (err) {
763
+ console.warn('Step 4c-10 (fail-soft): AsyncPollingService init warning:', err);
764
+ state._asyncPollingService = null;
765
+ }
766
+ // ------------------------------------------------------------------
767
+ // Step 4c-10.5: PositionTracker initialization (fail-soft)
768
+ // ------------------------------------------------------------------
769
+ try {
770
+ if (state.sqlite && state._settingsService) {
771
+ const trackerEnabled = state._settingsService.get('position_tracker.enabled');
772
+ if (trackerEnabled !== 'false') {
773
+ const { PositionTracker } = await import('../services/defi/position-tracker.js');
774
+ state.positionTracker = new PositionTracker({
775
+ sqlite: state.sqlite,
776
+ settingsService: state._settingsService,
777
+ rpcPool: state.rpcPool ?? undefined,
778
+ });
779
+ state.positionTracker.start();
780
+ console.debug('Step 4c-10.5: Position tracker started');
781
+ }
782
+ else {
783
+ console.debug('Step 4c-10.5: Position tracker disabled');
784
+ }
785
+ }
786
+ }
787
+ catch (err) {
788
+ console.warn('Step 4c-10.5 (fail-soft): Position tracker init warning:', err);
789
+ state.positionTracker = null;
790
+ }
791
+ // ------------------------------------------------------------------
792
+ // Step 4c-11: DeFiMonitorService initialization (fail-soft)
793
+ // ------------------------------------------------------------------
794
+ try {
795
+ const { DeFiMonitorService } = await import('../services/monitoring/defi-monitor-service.js');
796
+ state.defiMonitorService = new DeFiMonitorService();
797
+ // Register HealthFactorMonitor
798
+ if (state.sqlite) {
799
+ const { HealthFactorMonitor } = await import('../services/monitoring/health-factor-monitor.js');
800
+ const healthMonitor = new HealthFactorMonitor({
801
+ sqlite: state.sqlite,
802
+ notificationService: state.notificationService ?? undefined,
803
+ positionTracker: state.positionTracker ?? undefined,
804
+ });
805
+ state.defiMonitorService.register(healthMonitor);
806
+ }
807
+ // Register MaturityMonitor
808
+ if (state.sqlite) {
809
+ const { MaturityMonitor } = await import('../services/monitoring/maturity-monitor.js');
810
+ const maturityMonitor = new MaturityMonitor({
811
+ sqlite: state.sqlite,
812
+ eventBus: state.eventBus,
813
+ notificationService: state.notificationService ?? undefined,
814
+ });
815
+ if (state._settingsService) {
816
+ maturityMonitor.loadFromSettings(state._settingsService);
817
+ }
818
+ state.defiMonitorService.register(maturityMonitor);
819
+ }
820
+ // Register MarginMonitor
821
+ if (state.sqlite) {
822
+ const { MarginMonitor } = await import('../services/monitoring/margin-monitor.js');
823
+ const marginMonitor = new MarginMonitor({
824
+ sqlite: state.sqlite,
825
+ eventBus: state.eventBus,
826
+ notificationService: state.notificationService ?? undefined,
827
+ positionTracker: state.positionTracker ?? undefined,
828
+ });
829
+ if (state._settingsService) {
830
+ marginMonitor.loadFromSettings(state._settingsService);
831
+ }
832
+ state.defiMonitorService.register(marginMonitor);
833
+ }
834
+ state.defiMonitorService.start();
835
+ console.debug('Step 4c-11: DeFi monitor service started with', state.defiMonitorService.monitorCount, 'monitors');
836
+ }
837
+ catch (err) {
838
+ console.warn('Step 4c-11 (fail-soft): DeFi monitor service init warning:', err);
839
+ state.defiMonitorService = null;
840
+ }
841
+ // ------------------------------------------------------------------
842
+ // Step 4e: Price Oracle (fail-soft)
843
+ // ------------------------------------------------------------------
844
+ try {
845
+ const { InMemoryPriceCache, PythOracle, CoinGeckoOracle, OracleChain } = await import('../infrastructure/oracle/index.js');
846
+ const priceCache = new InMemoryPriceCache();
847
+ const pythOracle = new PythOracle();
848
+ const coingeckoApiKey = state._settingsService?.get('oracle.coingecko_api_key');
849
+ const coingeckoOracle = coingeckoApiKey
850
+ ? new CoinGeckoOracle(coingeckoApiKey)
851
+ : undefined;
852
+ const thresholdStr = state._settingsService?.get('oracle.cross_validation_threshold');
853
+ const crossValidationThreshold = thresholdStr ? Number(thresholdStr) : 5;
854
+ state.priceOracle = new OracleChain({
855
+ primary: pythOracle,
856
+ fallback: coingeckoOracle,
857
+ cache: priceCache,
858
+ crossValidationThreshold,
859
+ });
860
+ console.debug(`Step 4e: PriceOracle initialized (Pyth primary${coingeckoOracle ? ' + CoinGecko fallback' : ''})`);
861
+ }
862
+ catch (err) {
863
+ console.warn('Step 4e (fail-soft): PriceOracle init warning:', err);
864
+ state.priceOracle = undefined;
865
+ }
866
+ // ------------------------------------------------------------------
867
+ // Step 4e-2: ForexRateService (fail-soft)
868
+ // ------------------------------------------------------------------
869
+ try {
870
+ const { CoinGeckoForexProvider, ForexRateService, InMemoryPriceCache } = await import('../infrastructure/oracle/index.js');
871
+ const forexCache = new InMemoryPriceCache(30 * 60 * 1000, // TTL: 30 minutes
872
+ 2 * 60 * 60 * 1000, // staleMax: 2 hours
873
+ 64);
874
+ const coingeckoApiKey = state._settingsService?.get('oracle.coingecko_api_key') ?? '';
875
+ const forexProvider = new CoinGeckoForexProvider(coingeckoApiKey);
876
+ state.forexRateService = new ForexRateService({ forexProvider, cache: forexCache });
877
+ console.debug('Step 4e-2: ForexRateService initialized (30min cache)');
878
+ }
879
+ catch (err) {
880
+ console.warn('Step 4e-2 (fail-soft): ForexRateService init warning:', err);
881
+ state.forexRateService = null;
882
+ }
883
+ // ------------------------------------------------------------------
884
+ // Step 4f: ActionProviderRegistry (fail-soft)
885
+ // API keys are managed by SettingsService since v29.5 (#214)
886
+ // ------------------------------------------------------------------
887
+ try {
888
+ const { ActionProviderRegistry } = await import('../infrastructure/action/index.js');
889
+ state.actionProviderRegistry = new ActionProviderRegistry();
890
+ // Create IRpcCaller for Aave V3 using RpcPool eth_call.
891
+ // RpcPool.getUrl(network) provides priority-based URL rotation with cooldown.
892
+ const rpcCaller = state.rpcPool ? (() => {
893
+ const pool = state.rpcPool;
894
+ const networkMap = {
895
+ 1: 'ethereum-mainnet',
896
+ 42161: 'arbitrum-mainnet',
897
+ 10: 'optimism-mainnet',
898
+ 137: 'polygon-mainnet',
899
+ 8453: 'base-mainnet',
900
+ };
901
+ return {
902
+ call: async (params) => {
903
+ const network = params.chainId ? (networkMap[params.chainId] ?? 'ethereum-mainnet') : 'ethereum-mainnet';
904
+ const rpcUrl = pool.getUrl(network);
905
+ const resp = await fetch(rpcUrl, {
906
+ method: 'POST',
907
+ headers: { 'Content-Type': 'application/json' },
908
+ body: JSON.stringify({
909
+ jsonrpc: '2.0',
910
+ id: 1,
911
+ method: 'eth_call',
912
+ params: [{ to: params.to, data: params.data }, 'latest'],
913
+ }),
914
+ });
915
+ const json = await resp.json();
916
+ if (json.error)
917
+ throw new Error(json.error.message);
918
+ return json.result ?? '0x';
919
+ },
920
+ };
921
+ })() : undefined;
922
+ // Store rpcCaller for HotReloadOrchestrator use
923
+ state.rpcCaller = rpcCaller;
924
+ // Register built-in action providers from @waiaas/actions (reads from SettingsService)
925
+ const { registerBuiltInProviders } = await import('@waiaas/actions');
926
+ const builtIn = registerBuiltInProviders(state.actionProviderRegistry, state._settingsService, { rpcCaller });
927
+ // Capture HyperliquidMarketData for HTTP routes (Phase 349)
928
+ if (builtIn.hyperliquidMarketData) {
929
+ state.hyperliquidMarketData = builtIn.hyperliquidMarketData;
930
+ }
931
+ // Register Polymarket providers when enabled (Phase 373)
932
+ if (state._settingsService.get('actions.polymarket_enabled') === 'true') {
933
+ try {
934
+ const { createPolymarketInfrastructure } = await import('@waiaas/actions');
935
+ const { encryptSettingValue, decryptSettingValue } = await import('../infrastructure/settings/settings-crypto.js');
936
+ const { polymarketOrders: pmOrdersTable, polymarketPositions: pmPositionsTable, polymarketApiKeys: pmApiKeysTable, } = await import('../infrastructure/database/schema.js');
937
+ const { eq: drizzleEq } = await import('drizzle-orm');
938
+ const { uuidv7 } = await import('uuidv7');
939
+ const mpHash = state.masterPassword || '';
940
+ const db = state._db;
941
+ const now = () => Math.floor(Date.now() / 1000);
942
+ // Thin DB adapters wrapping Drizzle ORM for Polymarket interfaces (snake_case mapping)
943
+ const apiKeyDbAdapter = {
944
+ getApiKeyByWalletId: (walletId) => {
945
+ const row = db.select().from(pmApiKeysTable).where(drizzleEq(pmApiKeysTable.walletId, walletId)).get();
946
+ if (!row)
947
+ return null;
948
+ return { id: row.id, wallet_id: row.walletId, api_key: row.apiKey, api_secret_encrypted: row.apiSecretEncrypted, api_passphrase_encrypted: row.apiPassphraseEncrypted, signature_type: row.signatureType, proxy_address: row.proxyAddress, created_at: row.createdAt };
949
+ },
950
+ insertApiKey: (row) => db.insert(pmApiKeysTable).values({ id: uuidv7(), walletId: row.wallet_id, apiKey: row.api_key, apiSecretEncrypted: row.api_secret_encrypted, apiPassphraseEncrypted: row.api_passphrase_encrypted, signatureType: row.signature_type ?? 0, proxyAddress: row.proxy_address ?? null, createdAt: now() }).run(),
951
+ deleteApiKeyByWalletId: (walletId) => db.delete(pmApiKeysTable).where(drizzleEq(pmApiKeysTable.walletId, walletId)).run(),
952
+ };
953
+ const orderDbAdapter = {
954
+ insertOrder: (row) => db.insert(pmOrdersTable).values(row).run(),
955
+ updateOrderStatus: (id, status, updatedAt) => db.update(pmOrdersTable).set({ status, updatedAt }).where(drizzleEq(pmOrdersTable.id, id)).run(),
956
+ updateOrderStatusByOrderId: (orderId, status, updatedAt) => db.update(pmOrdersTable).set({ status, updatedAt }).where(drizzleEq(pmOrdersTable.orderId, orderId)).run(),
957
+ };
958
+ // PositionDb adapter matching the PositionDb interface
959
+ const positionDbAdapter = {
960
+ getPositions: (walletId) => {
961
+ const rows = db.select().from(pmPositionsTable).where(drizzleEq(pmPositionsTable.walletId, walletId)).all();
962
+ return rows.map(r => ({ id: r.id, wallet_id: r.walletId, condition_id: r.conditionId, token_id: r.tokenId, market_slug: r.marketSlug, outcome: r.outcome, size: r.size, avg_price: r.avgPrice, realized_pnl: r.realizedPnl, market_resolved: r.marketResolved, winning_outcome: r.winningOutcome, is_neg_risk: r.isNegRisk, created_at: r.createdAt, updated_at: r.updatedAt }));
963
+ },
964
+ getPosition: (walletId, tokenId) => {
965
+ const rows = db.select().from(pmPositionsTable).where(drizzleEq(pmPositionsTable.walletId, walletId)).all();
966
+ const row = rows.find(r => r.tokenId === tokenId);
967
+ if (!row)
968
+ return null;
969
+ return { id: row.id, wallet_id: row.walletId, condition_id: row.conditionId, token_id: row.tokenId, market_slug: row.marketSlug, outcome: row.outcome, size: row.size, avg_price: row.avgPrice, realized_pnl: row.realizedPnl, market_resolved: row.marketResolved, winning_outcome: row.winningOutcome, is_neg_risk: row.isNegRisk, created_at: row.createdAt, updated_at: row.updatedAt };
970
+ },
971
+ upsert: (row) => db.insert(pmPositionsTable).values(row)
972
+ .onConflictDoUpdate({ target: [pmPositionsTable.walletId, pmPositionsTable.tokenId], set: row }).run(),
973
+ updateResolution: (conditionId, winningOutcome) => db.update(pmPositionsTable).set({ marketResolved: 1, winningOutcome, updatedAt: now() }).where(drizzleEq(pmPositionsTable.conditionId, conditionId)).run(),
974
+ };
975
+ const encryptFn = (plaintext) => encryptSettingValue(plaintext, mpHash);
976
+ const decryptFn = (ciphertext) => decryptSettingValue(ciphertext, mpHash);
977
+ const pmInfra = createPolymarketInfrastructure({}, { apiKeys: apiKeyDbAdapter, orders: orderDbAdapter, positions: positionDbAdapter }, encryptFn, decryptFn);
978
+ state.actionProviderRegistry.register(pmInfra.orderProvider);
979
+ state.actionProviderRegistry.register(pmInfra.ctfProvider);
980
+ state.polymarketInfra = pmInfra;
981
+ console.debug('Step 4f-pm: Polymarket providers registered (order + ctf)');
982
+ }
983
+ catch (err) {
984
+ console.warn('Step 4f-pm (fail-soft): Polymarket registration failed:', err);
985
+ }
986
+ }
987
+ // Load plugins from ~/.waiaas/actions/ (if exists)
988
+ const actionsDir = join(dataDir, 'actions');
989
+ if (existsSync(actionsDir)) {
990
+ const result = await state.actionProviderRegistry.loadPlugins(actionsDir);
991
+ console.debug(`Step 4f: ActionProviderRegistry initialized (${builtIn.loaded.length} built-in, ${result.loaded.length} plugins loaded, ${result.failed.length} failed)`);
992
+ }
993
+ else {
994
+ console.debug(`Step 4f: ActionProviderRegistry initialized (${builtIn.loaded.length} built-in, no plugins directory)`);
995
+ }
996
+ }
997
+ catch (err) {
998
+ console.warn('Step 4f (fail-soft): ActionProviderRegistry init warning:', err);
999
+ }
1000
+ // ------------------------------------------------------------------
1001
+ // Step 4f-2: Register bridge status trackers when lifi is enabled
1002
+ // ------------------------------------------------------------------
1003
+ if (state._asyncPollingService && state._settingsService?.get('actions.lifi_enabled') === 'true') {
1004
+ try {
1005
+ const { BridgeStatusTracker, BridgeMonitoringTracker } = await import('@waiaas/actions');
1006
+ const lifiConfig = {
1007
+ enabled: true,
1008
+ apiBaseUrl: state._settingsService.get('actions.lifi_api_base_url'),
1009
+ apiKey: state._settingsService.get('actions.lifi_api_key'),
1010
+ defaultSlippagePct: Number(state._settingsService.get('actions.lifi_default_slippage_pct')),
1011
+ maxSlippagePct: Number(state._settingsService.get('actions.lifi_max_slippage_pct')),
1012
+ requestTimeoutMs: 15_000,
1013
+ };
1014
+ state._asyncPollingService.registerTracker(new BridgeStatusTracker(lifiConfig));
1015
+ state._asyncPollingService.registerTracker(new BridgeMonitoringTracker(lifiConfig));
1016
+ console.debug('Step 4f-2: Bridge status trackers registered (bridge + bridge-monitoring)');
1017
+ }
1018
+ catch (err) {
1019
+ console.warn('Step 4f-2 (fail-soft): Bridge tracker registration failed:', err);
1020
+ }
1021
+ }
1022
+ // ------------------------------------------------------------------
1023
+ // Step 4f-2a: Register Across bridge status trackers when across_bridge is enabled
1024
+ // ------------------------------------------------------------------
1025
+ if (state._asyncPollingService && state._settingsService?.get('actions.across_bridge_enabled') === 'true') {
1026
+ try {
1027
+ const { AcrossBridgeStatusTracker, AcrossBridgeMonitoringTracker } = await import('@waiaas/actions');
1028
+ const acrossConfig = {
1029
+ enabled: true,
1030
+ apiBaseUrl: state._settingsService.get('actions.across_bridge_api_base_url') || 'https://app.across.to/api',
1031
+ integratorId: state._settingsService.get('actions.across_bridge_integrator_id') || '',
1032
+ fillDeadlineBufferSec: Number(state._settingsService.get('actions.across_bridge_fill_deadline_buffer_sec')) || 21600,
1033
+ defaultSlippagePct: Number(state._settingsService.get('actions.across_bridge_default_slippage_pct')) || 0.01,
1034
+ maxSlippagePct: Number(state._settingsService.get('actions.across_bridge_max_slippage_pct')) || 0.03,
1035
+ requestTimeoutMs: 10_000,
1036
+ };
1037
+ state._asyncPollingService.registerTracker(new AcrossBridgeStatusTracker(acrossConfig));
1038
+ state._asyncPollingService.registerTracker(new AcrossBridgeMonitoringTracker(acrossConfig));
1039
+ console.debug('Step 4f-2a: Across bridge status trackers registered (across-bridge + across-bridge-monitoring)');
1040
+ }
1041
+ catch (err) {
1042
+ console.warn('Step 4f-2a (fail-soft): Across bridge tracker registration failed:', err);
1043
+ }
1044
+ }
1045
+ // ------------------------------------------------------------------
1046
+ // Step 4f-3: Register staking status trackers when lido/jito is enabled
1047
+ // ------------------------------------------------------------------
1048
+ if (state._asyncPollingService) {
1049
+ try {
1050
+ if (state._settingsService?.get('actions.lido_staking_enabled') === 'true') {
1051
+ const { LidoWithdrawalTracker } = await import('@waiaas/actions');
1052
+ state._asyncPollingService.registerTracker(new LidoWithdrawalTracker());
1053
+ console.debug('Step 4f-3: Lido withdrawal tracker registered');
1054
+ }
1055
+ if (state._settingsService?.get('actions.jito_staking_enabled') === 'true') {
1056
+ const { JitoEpochTracker } = await import('@waiaas/actions');
1057
+ state._asyncPollingService.registerTracker(new JitoEpochTracker());
1058
+ console.debug('Step 4f-3: Jito epoch tracker registered');
1059
+ }
1060
+ }
1061
+ catch (err) {
1062
+ console.warn('Step 4f-3 (fail-soft): Staking tracker registration failed:', err);
1063
+ }
1064
+ }
1065
+ // ------------------------------------------------------------------
1066
+ // Step 4f-4: Register GasConditionTracker (gas price condition monitoring)
1067
+ // ------------------------------------------------------------------
1068
+ if (state._asyncPollingService) {
1069
+ try {
1070
+ const gasConditionEnabled = state._settingsService?.get('gas_condition.enabled') !== 'false';
1071
+ if (gasConditionEnabled) {
1072
+ const { GasConditionTracker } = await import('../pipeline/gas-condition-tracker.js');
1073
+ state._asyncPollingService.registerTracker(new GasConditionTracker());
1074
+ console.debug('Step 4f-4: GasConditionTracker registered');
1075
+ }
1076
+ else {
1077
+ console.debug('Step 4f-4: GasConditionTracker disabled');
1078
+ }
1079
+ }
1080
+ catch (err) {
1081
+ console.warn('Step 4f-4 (fail-soft): GasConditionTracker registration failed:', err);
1082
+ }
1083
+ }
1084
+ // ------------------------------------------------------------------
1085
+ // Step 4f-6: Register IPositionProvider implementations with PositionTracker
1086
+ // ------------------------------------------------------------------
1087
+ if (state.positionTracker && state.actionProviderRegistry) {
1088
+ try {
1089
+ // Check each registered provider for IPositionProvider interface (duck-typing)
1090
+ for (const meta of state.actionProviderRegistry.listProviders()) {
1091
+ const provider = state.actionProviderRegistry.getProvider(meta.name);
1092
+ if (provider && 'getPositions' in provider && 'getSupportedCategories' in provider && 'getProviderName' in provider) {
1093
+ state.positionTracker.registerProvider(provider);
1094
+ console.debug(`Step 4f-5: Registered ${meta.name} with PositionTracker`);
1095
+ }
1096
+ }
1097
+ console.debug(`Step 4f-5: PositionTracker has ${state.positionTracker.providerCount} providers`);
1098
+ // Trigger immediate sync for all categories now that providers are registered
1099
+ if (state.positionTracker.providerCount > 0) {
1100
+ void state.positionTracker.syncCategory('LENDING');
1101
+ void state.positionTracker.syncCategory('STAKING');
1102
+ void state.positionTracker.syncCategory('YIELD');
1103
+ void state.positionTracker.syncCategory('PERP');
1104
+ }
1105
+ }
1106
+ catch (err) {
1107
+ console.warn('Step 4f-5 (fail-soft): PositionTracker provider registration warning:', err);
1108
+ }
1109
+ }
1110
+ // ------------------------------------------------------------------
1111
+ // Step 4g: VersionCheckService (create before Step 5 for Health endpoint)
1112
+ // ------------------------------------------------------------------
1113
+ if (state.sqlite && state._config.daemon.update_check) {
1114
+ const { VersionCheckService } = await import('../infrastructure/version/index.js');
1115
+ state._versionCheckService = new VersionCheckService(state.sqlite);
1116
+ if (state.notificationService) {
1117
+ state._versionCheckService.setNotificationService(state.notificationService);
1118
+ }
1119
+ console.debug('Step 4g: VersionCheckService created');
1120
+ }
1121
+ // ------------------------------------------------------------------
1122
+ // Step 4h: EncryptedBackupService (fail-soft)
1123
+ // ------------------------------------------------------------------
1124
+ try {
1125
+ if (state.sqlite) {
1126
+ const { isAbsolute } = await import('node:path');
1127
+ const { EncryptedBackupService } = await import('../infrastructure/backup/encrypted-backup-service.js');
1128
+ const backupDir = state._config.backup?.dir ?? 'backups';
1129
+ const backupsDir = isAbsolute(backupDir) ? backupDir : join(dataDir, backupDir);
1130
+ state._encryptedBackupService = new EncryptedBackupService(dataDir, backupsDir, state.sqlite);
1131
+ console.debug('Step 4h: EncryptedBackupService created');
1132
+ }
1133
+ }
1134
+ catch (err) {
1135
+ console.warn('Step 4h (fail-soft): EncryptedBackupService init warning:', err);
1136
+ }
1137
+ // ------------------------------------------------------------------
1138
+ // Step 4i: WebhookService (fail-soft)
1139
+ // ------------------------------------------------------------------
1140
+ try {
1141
+ if (state.sqlite && state.eventBus) {
1142
+ const { WebhookService } = await import('../services/webhook-service.js');
1143
+ state.webhookService = new WebhookService(state.sqlite, state.eventBus, () => state.masterPassword);
1144
+ console.debug('Step 4i: WebhookService created');
1145
+ }
1146
+ }
1147
+ catch (err) {
1148
+ console.warn('Step 4i (fail-soft): WebhookService init warning:', err);
1149
+ }
1150
+ // ------------------------------------------------------------------
1151
+ // Step 5: HTTP server start (5s, fail-fast)
1152
+ // ------------------------------------------------------------------
1153
+ await withTimeout((async () => {
1154
+ const { createApp } = await import('../api/index.js');
1155
+ const { serve } = await import('@hono/node-server');
1156
+ const { HotReloadOrchestrator } = await import('../infrastructure/settings/index.js');
1157
+ const hotReloader = new HotReloadOrchestrator({
1158
+ settingsService: state._settingsService,
1159
+ notificationService: state.notificationService,
1160
+ adapterPool: state.adapterPool,
1161
+ autoStopService: state.autoStopService,
1162
+ balanceMonitorService: state.balanceMonitorService,
1163
+ wcServiceRef: state.wcServiceRef,
1164
+ wcSigningBridgeRef: state.wcSigningBridgeRef,
1165
+ approvalWorkflow: state.approvalWorkflow,
1166
+ sqlite: state.sqlite,
1167
+ telegramBotRef: state.telegramBotRef,
1168
+ killSwitchService: state.killSwitchService,
1169
+ incomingTxMonitorService: state.incomingTxMonitorService,
1170
+ actionProviderRegistryRef: { current: state.actionProviderRegistry },
1171
+ rpcCaller: state.rpcCaller ?? undefined,
1172
+ });
1173
+ // [Phase 320] Create ReputationCacheService for REPUTATION_THRESHOLD policy evaluation
1174
+ const { ReputationCacheService } = await import('../services/erc8004/index.js');
1175
+ const reputationCacheService = new ReputationCacheService(state._db, state._settingsService ?? undefined);
1176
+ // [#272] Create SmartAccountService for ERC-4337 CREATE2 address prediction
1177
+ const { SmartAccountService } = await import('../infrastructure/smart-account/smart-account-service.js');
1178
+ const smartAccountService = new SmartAccountService();
1179
+ // [Phase 390] Bootstrap signer capabilities for external action signing
1180
+ const { SignerCapabilityRegistry } = await import('../signing/registry.js');
1181
+ const { bootstrapSignerCapabilities } = await import('../signing/bootstrap.js');
1182
+ const signerRegistry = new SignerCapabilityRegistry();
1183
+ bootstrapSignerCapabilities(signerRegistry);
1184
+ // [Phase 422] v32.0: ContractNameRegistry for notification enrichment
1185
+ const { ContractNameRegistry } = await import('@waiaas/core');
1186
+ state.contractNameRegistry = new ContractNameRegistry();
1187
+ const app = createApp({
1188
+ db: state._db,
1189
+ sqlite: state.sqlite ?? undefined,
1190
+ keyStore: state.keyStore,
1191
+ masterPassword: state.masterPassword,
1192
+ masterPasswordHash: state.masterPasswordHash || undefined,
1193
+ passwordRef: state.passwordRef ?? undefined,
1194
+ config: state._config,
1195
+ adapterPool: state.adapterPool,
1196
+ policyEngine: new DatabasePolicyEngine(state._db, state.sqlite ?? undefined, state._settingsService ?? undefined, reputationCacheService),
1197
+ reputationCache: reputationCacheService,
1198
+ jwtSecretManager: state.jwtSecretManager ?? undefined,
1199
+ delayQueue: state.delayQueue ?? undefined,
1200
+ approvalWorkflow: state.approvalWorkflow ?? undefined,
1201
+ notificationService: state.notificationService ?? undefined,
1202
+ settingsService: state._settingsService ?? undefined,
1203
+ priceOracle: state.priceOracle,
1204
+ actionProviderRegistry: state.actionProviderRegistry ?? undefined,
1205
+ smartAccountService,
1206
+ // apiKeyStore removed in v29.5 -- API keys via SettingsService
1207
+ onSettingsChanged: (changedKeys) => {
1208
+ void hotReloader.handleChangedKeys(changedKeys);
1209
+ },
1210
+ dataDir,
1211
+ forexRateService: state.forexRateService ?? undefined,
1212
+ eventBus: state.eventBus,
1213
+ killSwitchService: state.killSwitchService ?? undefined,
1214
+ wcServiceRef: state.wcServiceRef,
1215
+ wcSigningBridgeRef: state.wcSigningBridgeRef,
1216
+ approvalChannelRouter: state.approvalChannelRouter ?? undefined,
1217
+ versionCheckService: state._versionCheckService,
1218
+ encryptedBackupService: state._encryptedBackupService ?? undefined,
1219
+ adminStatsService: state.adminStatsService ?? undefined,
1220
+ autoStopService: state.autoStopService ?? undefined,
1221
+ metricsCounter: state.metricsCounter ?? undefined,
1222
+ hyperliquidMarketData: state.hyperliquidMarketData ?? undefined,
1223
+ polymarketInfra: state.polymarketInfra ?? undefined,
1224
+ signerRegistry,
1225
+ contractNameRegistry: state.contractNameRegistry ?? undefined,
1226
+ });
1227
+ const hostname = state._config.daemon.hostname;
1228
+ const port = state._config.daemon.port;
1229
+ const server = serve({
1230
+ fetch: app.fetch,
1231
+ hostname,
1232
+ port,
1233
+ });
1234
+ state.httpServer = server;
1235
+ // v31.14: Long-poll RPC proxy support -- keep connections alive for 10 minutes
1236
+ // Default Node.js keepAliveTimeout is 5s, which is too short for DELAY/APPROVAL tier
1237
+ // long-poll responses that can take up to 600s.
1238
+ // Hono serve() returns http.Server which exposes these timeout properties.
1239
+ const httpServer = server;
1240
+ httpServer.keepAliveTimeout = 600_000; // 600 seconds in milliseconds
1241
+ httpServer.headersTimeout = 605_000; // Must be > keepAliveTimeout (Node.js docs)
1242
+ // Wait for server to actually start listening (catches EADDRINUSE)
1243
+ await new Promise((resolve, reject) => {
1244
+ const onListening = () => {
1245
+ server.removeListener('error', onError);
1246
+ resolve();
1247
+ };
1248
+ const onError = (err) => {
1249
+ server.removeListener('listening', onListening);
1250
+ if (err.code === 'EADDRINUSE') {
1251
+ reject(new Error(`Port ${port} is already in use. Try a different port or stop the other process.`));
1252
+ }
1253
+ else {
1254
+ reject(err);
1255
+ }
1256
+ };
1257
+ // @hono/node-server serve() returns a Node.js http.Server
1258
+ server.once('listening', onListening);
1259
+ server.once('error', onError);
1260
+ });
1261
+ console.debug(`Step 5: HTTP server listening on ${hostname}:${port}`);
1262
+ })(), 5_000, 'STEP5_HTTP_SERVER');
1263
+ // ------------------------------------------------------------------
1264
+ // Step 6: Background workers + PID (no timeout, fail-soft)
1265
+ // ------------------------------------------------------------------
1266
+ await startWorkers(state, dataDir);
1267
+ }
1268
+ /**
1269
+ * Step 6: Register and start all background workers, write PID file.
1270
+ */
1271
+ async function startWorkers(state, dataDir) {
1272
+ try {
1273
+ // Ensure workers instance exists (should already be created before Step 4c-9)
1274
+ if (!state.workers) {
1275
+ state.workers = new BackgroundWorkers();
1276
+ }
1277
+ // Register WAL checkpoint worker (default: 5 min = 300s)
1278
+ const walInterval = state._config.database.wal_checkpoint_interval * 1000;
1279
+ state.workers.register('wal-checkpoint', {
1280
+ interval: walInterval,
1281
+ handler: () => {
1282
+ if (state.sqlite && !state._isShuttingDown) {
1283
+ state.sqlite.pragma('wal_checkpoint(PASSIVE)');
1284
+ }
1285
+ },
1286
+ });
1287
+ // Register session cleanup worker (1 min = 60s)
1288
+ state.workers.register('session-cleanup', {
1289
+ interval: 60_000,
1290
+ handler: () => {
1291
+ if (state.sqlite && !state._isShuttingDown) {
1292
+ // Notify expired sessions before deletion (fire-and-forget)
1293
+ if (state.notificationService) {
1294
+ try {
1295
+ const expired = state.sqlite.prepare("SELECT id, wallet_id FROM sessions WHERE expires_at > 0 AND expires_at < unixepoch() AND revoked_at IS NULL").all();
1296
+ for (const session of expired) {
1297
+ void state.notificationService.notify('SESSION_EXPIRED', session.wallet_id, {
1298
+ sessionId: session.id,
1299
+ });
1300
+ }
1301
+ }
1302
+ catch {
1303
+ // Fire-and-forget: never block cleanup
1304
+ }
1305
+ }
1306
+ state.sqlite.exec("DELETE FROM sessions WHERE expires_at > 0 AND expires_at < unixepoch() AND revoked_at IS NULL");
1307
+ }
1308
+ },
1309
+ });
1310
+ // Register delay-expired worker (every 5s: check for expired DELAY transactions)
1311
+ // #327: Process expired items sequentially with concurrency limit to prevent
1312
+ // resource exhaustion from concurrent RPC calls when many items expire at once.
1313
+ if (state.delayQueue) {
1314
+ state.workers.register('delay-expired', {
1315
+ interval: 5_000,
1316
+ handler: () => {
1317
+ if (state._isShuttingDown)
1318
+ return;
1319
+ const now = Math.floor(Date.now() / 1000);
1320
+ const expired = state.delayQueue.processExpired(now);
1321
+ if (expired.length === 0)
1322
+ return;
1323
+ // Process sequentially (one at a time) to avoid concurrent RPC/memory pressure
1324
+ void (async () => {
1325
+ for (const tx of expired) {
1326
+ if (state._isShuttingDown)
1327
+ break;
1328
+ try {
1329
+ await state.executeFromStage5(tx.txId, tx.walletId);
1330
+ }
1331
+ catch (err) {
1332
+ console.error(`[delay-expired] executeFromStage5(${tx.txId}) error:`, err);
1333
+ }
1334
+ }
1335
+ })();
1336
+ },
1337
+ });
1338
+ }
1339
+ // Register approval-expired worker (every 30s: expire timed-out approvals)
1340
+ if (state.approvalWorkflow) {
1341
+ state.workers.register('approval-expired', {
1342
+ interval: 30_000,
1343
+ handler: () => {
1344
+ if (state._isShuttingDown)
1345
+ return;
1346
+ const now = Math.floor(Date.now() / 1000);
1347
+ state.approvalWorkflow.processExpiredApprovals(now);
1348
+ },
1349
+ });
1350
+ }
1351
+ // #329: Register submitted-tx-confirm worker (every 60s)
1352
+ // Retries confirmation for transactions stuck in SUBMITTED state after Stage 6 timeout.
1353
+ // Prevents STO-03 regression where on-chain success is not reflected in DB status.
1354
+ state.workers.register('submitted-tx-confirm', {
1355
+ interval: 60_000,
1356
+ handler: async () => {
1357
+ if (state._isShuttingDown || !state._db || !state.adapterPool || !state.sqlite)
1358
+ return;
1359
+ try {
1360
+ const { transactions } = await import('../infrastructure/database/schema.js');
1361
+ const { eq, and, isNotNull } = await import('drizzle-orm');
1362
+ const { insertAuditLog } = await import('../infrastructure/database/audit-helper.js');
1363
+ // Find SUBMITTED transactions with txHash that are older than 60s
1364
+ const cutoff = Math.floor(Date.now() / 1000) - 60;
1365
+ const stuckTxs = state._db
1366
+ .select({
1367
+ id: transactions.id,
1368
+ txHash: transactions.txHash,
1369
+ walletId: transactions.walletId,
1370
+ chain: transactions.chain,
1371
+ network: transactions.network,
1372
+ amount: transactions.amount,
1373
+ toAddress: transactions.toAddress,
1374
+ type: transactions.type,
1375
+ })
1376
+ .from(transactions)
1377
+ .where(and(eq(transactions.status, 'SUBMITTED'), isNotNull(transactions.txHash)))
1378
+ .all()
1379
+ .filter((tx) => {
1380
+ // Only retry if created before cutoff (avoid racing with Stage 6)
1381
+ const meta = state.sqlite.prepare('SELECT created_at FROM transactions WHERE id = ?').get(tx.id);
1382
+ return !meta?.created_at || meta.created_at < cutoff;
1383
+ });
1384
+ for (const tx of stuckTxs) {
1385
+ if (state._isShuttingDown || !tx.txHash || !tx.network)
1386
+ continue;
1387
+ try {
1388
+ const rpcUrl = resolveRpcUrl(state._config.rpc, tx.chain, tx.network);
1389
+ const adapter = await state.adapterPool.resolve(tx.chain, tx.network, rpcUrl);
1390
+ const result = await adapter.waitForConfirmation(tx.txHash, 10_000);
1391
+ if (result.status === 'confirmed' || result.status === 'finalized') {
1392
+ const executedAt = new Date(Math.floor(Date.now() / 1000) * 1000);
1393
+ state._db
1394
+ .update(transactions)
1395
+ .set({ status: 'CONFIRMED', executedAt })
1396
+ .where(eq(transactions.id, tx.id))
1397
+ .run();
1398
+ insertAuditLog(state.sqlite, {
1399
+ eventType: 'TX_CONFIRMED',
1400
+ actor: 'system',
1401
+ walletId: tx.walletId,
1402
+ txId: tx.id,
1403
+ details: { txHash: tx.txHash, source: 'submitted-tx-confirm-worker', network: tx.network },
1404
+ severity: 'info',
1405
+ });
1406
+ void state.notificationService?.notify('TX_CONFIRMED', tx.walletId, {
1407
+ txId: tx.id,
1408
+ txHash: tx.txHash,
1409
+ network: tx.network,
1410
+ amount: tx.amount ?? '',
1411
+ to: tx.toAddress ?? '',
1412
+ type: tx.type ?? '',
1413
+ }, { txId: tx.id });
1414
+ console.info(`[submitted-tx-confirm] ${tx.id} confirmed via background retry`);
1415
+ }
1416
+ else if (result.status === 'failed') {
1417
+ state._db
1418
+ .update(transactions)
1419
+ .set({ status: 'FAILED', error: 'Transaction reverted on-chain (background check)' })
1420
+ .where(eq(transactions.id, tx.id))
1421
+ .run();
1422
+ console.warn(`[submitted-tx-confirm] ${tx.id} failed on-chain`);
1423
+ }
1424
+ // status === 'submitted': still pending, retry on next interval
1425
+ }
1426
+ catch (_err) {
1427
+ // Swallow individual tx errors (RPC timeout, rate limit) -- will retry next interval
1428
+ }
1429
+ }
1430
+ }
1431
+ catch (err) {
1432
+ console.error('[submitted-tx-confirm] worker error:', err);
1433
+ }
1434
+ },
1435
+ });
1436
+ // Register userop-build-cleanup worker (5 min = 300s)
1437
+ // Deletes expired build records from userop_builds table (10-min TTL)
1438
+ state.workers.register('userop-build-cleanup', {
1439
+ interval: 300_000,
1440
+ handler: () => {
1441
+ if (state.sqlite && !state._isShuttingDown) {
1442
+ const now = Math.floor(Date.now() / 1000);
1443
+ state.sqlite.prepare('DELETE FROM userop_builds WHERE expires_at < ?').run(now);
1444
+ }
1445
+ },
1446
+ });
1447
+ // Register credential-cleanup worker (5 min = 300s)
1448
+ // Deletes expired credentials from wallet_credentials table
1449
+ state.workers.register('credential-cleanup', {
1450
+ interval: 300_000,
1451
+ handler: () => {
1452
+ if (state.sqlite && !state._isShuttingDown) {
1453
+ const now = Math.floor(Date.now() / 1000);
1454
+ const result = state.sqlite.prepare('DELETE FROM wallet_credentials WHERE expires_at IS NOT NULL AND expires_at < ?').run(now);
1455
+ if (result.changes > 0) {
1456
+ console.log(`[credential-cleanup] Deleted ${result.changes} expired credential(s)`);
1457
+ }
1458
+ }
1459
+ },
1460
+ });
1461
+ // Register async-status polling worker (30s)
1462
+ if (state._asyncPollingService) {
1463
+ const pollingService = state._asyncPollingService;
1464
+ state.workers.register('async-status', {
1465
+ interval: 30_000,
1466
+ handler: async () => {
1467
+ if (state._isShuttingDown)
1468
+ return;
1469
+ await pollingService.pollAll();
1470
+ },
1471
+ });
1472
+ }
1473
+ // Register version-check worker (uses instance created in Step 4g)
1474
+ if (state._versionCheckService) {
1475
+ const versionCheckInterval = state._config.daemon.update_check_interval * 1000;
1476
+ state.workers.register('version-check', {
1477
+ interval: versionCheckInterval,
1478
+ runImmediately: true,
1479
+ handler: async () => { await state._versionCheckService.check(); },
1480
+ });
1481
+ console.debug('Step 6: Version check worker registered');
1482
+ }
1483
+ else {
1484
+ console.debug('Step 6: Version check disabled');
1485
+ }
1486
+ // Register backup worker (auto-backup scheduler)
1487
+ if (state._encryptedBackupService && state._config.backup.interval > 0) {
1488
+ const backupInterval = state._config.backup.interval * 1000; // seconds -> ms
1489
+ const retentionCount = state._config.backup.retention_count;
1490
+ const backupService = state._encryptedBackupService;
1491
+ const masterPwd = state.masterPassword;
1492
+ state.workers.register('backup-worker', {
1493
+ interval: backupInterval,
1494
+ handler: async () => {
1495
+ if (state._isShuttingDown)
1496
+ return;
1497
+ try {
1498
+ const info = await backupService.createBackup(masterPwd);
1499
+ console.log(`Auto-backup created: ${info.filename} (${info.size} bytes)`);
1500
+ const pruned = backupService.pruneBackups(retentionCount);
1501
+ if (pruned > 0) {
1502
+ console.log(`Auto-backup: pruned ${pruned} old backup(s), keeping ${retentionCount}`);
1503
+ }
1504
+ }
1505
+ catch (err) {
1506
+ console.error('Auto-backup failed:', err);
1507
+ }
1508
+ },
1509
+ });
1510
+ console.debug(`Step 6: Backup worker registered (interval=${state._config.backup.interval}s, retention=${retentionCount})`);
1511
+ }
1512
+ else {
1513
+ console.debug('Step 6: Backup worker disabled (interval=0 or no backup service)');
1514
+ }
1515
+ state.workers.startAll();
1516
+ // Write PID file
1517
+ state.pidPath = join(dataDir, state._config.daemon.pid_file);
1518
+ writeFileSync(state.pidPath, String(process.pid), 'utf-8');
1519
+ console.debug(`Step 6: Workers started, PID file written`);
1520
+ console.log(`WAIaaS daemon ready on http://${state._config.daemon.hostname}:${state._config.daemon.port} (PID: ${process.pid})\n` +
1521
+ ` Admin UI: http://${state._config.daemon.hostname}:${state._config.daemon.port}/admin`);
1522
+ }
1523
+ catch (err) {
1524
+ console.warn('Step 6 (fail-soft): Worker/PID warning:', err);
1525
+ }
1526
+ }
1527
+ //# sourceMappingURL=daemon-startup.js.map