gnoman 0.1.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 (606) hide show
  1. package/.eslintrc.cjs +24 -0
  2. package/.gnoman/contracts.json +4094 -0
  3. package/.gnoman/exec_package/runtime-debug.jsonl +45 -0
  4. package/.gnoman/holds.sqlite +0 -0
  5. package/.gnoman/license.json +7 -0
  6. package/.gnoman/safes.json +37 -0
  7. package/.gnoman/vanity-jobs.json +67 -0
  8. package/.gnoman/wallets.db +0 -0
  9. package/.prettierrc.json +6 -0
  10. package/CODex_TASKS.md +124 -0
  11. package/LICENSE.md +164 -0
  12. package/README.md +95 -0
  13. package/assets/GnoLogo.jpg +0 -0
  14. package/assets/self.png +0 -0
  15. package/backend/controllers/contractController.ts +49 -0
  16. package/backend/controllers/devToolsController.ts +76 -0
  17. package/backend/controllers/etherscanController.ts +59 -0
  18. package/backend/controllers/historyController.ts +7 -0
  19. package/backend/controllers/keyringController.ts +134 -0
  20. package/backend/controllers/robinhoodController.ts +80 -0
  21. package/backend/controllers/safeController.ts +167 -0
  22. package/backend/controllers/sandboxController.ts +63 -0
  23. package/backend/controllers/settingsController.ts +38 -0
  24. package/backend/controllers/walletController.ts +151 -0
  25. package/backend/index.ts +133 -0
  26. package/backend/licenses/license_public.pem +3 -0
  27. package/backend/licenses/verify_license.py +43 -0
  28. package/backend/routes/contractRoutes.ts +11 -0
  29. package/backend/routes/devToolsRoutes.ts +11 -0
  30. package/backend/routes/etherscanRoutes.ts +11 -0
  31. package/backend/routes/historyRoutes.ts +8 -0
  32. package/backend/routes/keyringRoutes.ts +25 -0
  33. package/backend/routes/licenseRoutes.ts +35 -0
  34. package/backend/routes/robinhoodRoutes.ts +22 -0
  35. package/backend/routes/runtimeRoutes.ts +29 -0
  36. package/backend/routes/safeRoutes.ts +28 -0
  37. package/backend/routes/sandboxRoutes.ts +17 -0
  38. package/backend/routes/settingsRoutes.ts +14 -0
  39. package/backend/routes/walletRoutes.ts +21 -0
  40. package/backend/services/chainlinkService.ts +65 -0
  41. package/backend/services/contractRegistryService.ts +205 -0
  42. package/backend/services/devToolsService.ts +251 -0
  43. package/backend/services/diagnosticsService.ts +350 -0
  44. package/backend/services/etherscanService.ts +152 -0
  45. package/backend/services/historyService.ts +89 -0
  46. package/backend/services/keyringAccessor.ts +4 -0
  47. package/backend/services/licenseService.ts +163 -0
  48. package/backend/services/onchain/abiRegistry.ts +57 -0
  49. package/backend/services/onchain/chainlinkClient.ts +56 -0
  50. package/backend/services/onchain/errors.ts +16 -0
  51. package/backend/services/onchain/etherscanClient.ts +94 -0
  52. package/backend/services/onchain/index.ts +76 -0
  53. package/backend/services/onchain/tenderlyRpcClient.ts +74 -0
  54. package/backend/services/onchain/types.ts +33 -0
  55. package/backend/services/onchainAutomationService.ts +424 -0
  56. package/backend/services/robinhood/auth.ts +42 -0
  57. package/backend/services/robinhood/client.ts +123 -0
  58. package/backend/services/robinhood/integrationService.ts +140 -0
  59. package/backend/services/robinhood/provider.ts +22 -0
  60. package/backend/services/robinhood/unofficialClient.ts +66 -0
  61. package/backend/services/rpcService.ts +44 -0
  62. package/backend/services/runtimeTelemetryService.ts +158 -0
  63. package/backend/services/safeConfigRepository.ts +205 -0
  64. package/backend/services/safeService.ts +588 -0
  65. package/backend/services/sandboxService.ts +157 -0
  66. package/backend/services/secureSettingsService.ts +45 -0
  67. package/backend/services/transactionHoldService.ts +223 -0
  68. package/backend/services/vanityService.ts +293 -0
  69. package/backend/services/walletService.ts +290 -0
  70. package/backend/services/walletStore.ts +179 -0
  71. package/backend/types/express-async-handler.d.ts +13 -0
  72. package/backend/types/keyring.d.ts +19 -0
  73. package/backend/utils/abiResolver.ts +208 -0
  74. package/backend/utils/http.ts +6 -0
  75. package/backend/utils/secretsResolver.ts +150 -0
  76. package/backend/utils/signer.ts +11 -0
  77. package/backend/workers/vanityWorker.ts +76 -0
  78. package/capacitor.config.ts +13 -0
  79. package/cli/gnoman.ts +424 -0
  80. package/contracts/OracleConsumer.sol +20 -0
  81. package/contracts/PriceFeedConsumer.sol +22 -0
  82. package/dist/backend/backend/controllers/contractController.js +41 -0
  83. package/dist/backend/backend/controllers/contractController.js.map +1 -0
  84. package/dist/backend/backend/controllers/devToolsController.js +63 -0
  85. package/dist/backend/backend/controllers/devToolsController.js.map +1 -0
  86. package/dist/backend/backend/controllers/etherscanController.js +53 -0
  87. package/dist/backend/backend/controllers/etherscanController.js.map +1 -0
  88. package/dist/backend/backend/controllers/historyController.js +12 -0
  89. package/dist/backend/backend/controllers/historyController.js.map +1 -0
  90. package/dist/backend/backend/controllers/keyringController.js +126 -0
  91. package/dist/backend/backend/controllers/keyringController.js.map +1 -0
  92. package/dist/backend/backend/controllers/robinhoodController.js +69 -0
  93. package/dist/backend/backend/controllers/robinhoodController.js.map +1 -0
  94. package/dist/backend/backend/controllers/safeController.js +137 -0
  95. package/dist/backend/backend/controllers/safeController.js.map +1 -0
  96. package/dist/backend/backend/controllers/sandboxController.js +48 -0
  97. package/dist/backend/backend/controllers/sandboxController.js.map +1 -0
  98. package/dist/backend/backend/controllers/settingsController.js +34 -0
  99. package/dist/backend/backend/controllers/settingsController.js.map +1 -0
  100. package/dist/backend/backend/controllers/walletController.js +140 -0
  101. package/dist/backend/backend/controllers/walletController.js.map +1 -0
  102. package/dist/backend/backend/index.js +119 -0
  103. package/dist/backend/backend/index.js.map +1 -0
  104. package/dist/backend/backend/routes/contractRoutes.js +44 -0
  105. package/dist/backend/backend/routes/contractRoutes.js.map +1 -0
  106. package/dist/backend/backend/routes/devToolsRoutes.js +44 -0
  107. package/dist/backend/backend/routes/devToolsRoutes.js.map +1 -0
  108. package/dist/backend/backend/routes/etherscanRoutes.js +44 -0
  109. package/dist/backend/backend/routes/etherscanRoutes.js.map +1 -0
  110. package/dist/backend/backend/routes/historyRoutes.js +41 -0
  111. package/dist/backend/backend/routes/historyRoutes.js.map +1 -0
  112. package/dist/backend/backend/routes/keyringRoutes.js +18 -0
  113. package/dist/backend/backend/routes/keyringRoutes.js.map +1 -0
  114. package/dist/backend/backend/routes/licenseRoutes.js +30 -0
  115. package/dist/backend/backend/routes/licenseRoutes.js.map +1 -0
  116. package/dist/backend/backend/routes/robinhoodRoutes.js +14 -0
  117. package/dist/backend/backend/routes/robinhoodRoutes.js.map +1 -0
  118. package/dist/backend/backend/routes/runtimeRoutes.js +26 -0
  119. package/dist/backend/backend/routes/runtimeRoutes.js.map +1 -0
  120. package/dist/backend/backend/routes/safeRoutes.js +61 -0
  121. package/dist/backend/backend/routes/safeRoutes.js.map +1 -0
  122. package/dist/backend/backend/routes/sandboxRoutes.js +50 -0
  123. package/dist/backend/backend/routes/sandboxRoutes.js.map +1 -0
  124. package/dist/backend/backend/routes/settingsRoutes.js +10 -0
  125. package/dist/backend/backend/routes/settingsRoutes.js.map +1 -0
  126. package/dist/backend/backend/routes/walletRoutes.js +54 -0
  127. package/dist/backend/backend/routes/walletRoutes.js.map +1 -0
  128. package/dist/backend/backend/services/chainlinkService.js +48 -0
  129. package/dist/backend/backend/services/chainlinkService.js.map +1 -0
  130. package/dist/backend/backend/services/contractRegistryService.js +138 -0
  131. package/dist/backend/backend/services/contractRegistryService.js.map +1 -0
  132. package/dist/backend/backend/services/devToolsService.js +213 -0
  133. package/dist/backend/backend/services/devToolsService.js.map +1 -0
  134. package/dist/backend/backend/services/diagnosticsService.js +286 -0
  135. package/dist/backend/backend/services/diagnosticsService.js.map +1 -0
  136. package/dist/backend/backend/services/etherscanService.js +125 -0
  137. package/dist/backend/backend/services/etherscanService.js.map +1 -0
  138. package/dist/backend/backend/services/historyService.js +75 -0
  139. package/dist/backend/backend/services/historyService.js.map +1 -0
  140. package/dist/backend/backend/services/keyringAccessor.js +40 -0
  141. package/dist/backend/backend/services/keyringAccessor.js.map +1 -0
  142. package/dist/backend/backend/services/licenseService.js +130 -0
  143. package/dist/backend/backend/services/licenseService.js.map +1 -0
  144. package/dist/backend/backend/services/onchain/abiRegistry.js +47 -0
  145. package/dist/backend/backend/services/onchain/abiRegistry.js.map +1 -0
  146. package/dist/backend/backend/services/onchain/chainlinkClient.js +43 -0
  147. package/dist/backend/backend/services/onchain/chainlinkClient.js.map +1 -0
  148. package/dist/backend/backend/services/onchain/errors.js +13 -0
  149. package/dist/backend/backend/services/onchain/errors.js.map +1 -0
  150. package/dist/backend/backend/services/onchain/etherscanClient.js +82 -0
  151. package/dist/backend/backend/services/onchain/etherscanClient.js.map +1 -0
  152. package/dist/backend/backend/services/onchain/index.js +79 -0
  153. package/dist/backend/backend/services/onchain/index.js.map +1 -0
  154. package/dist/backend/backend/services/onchain/tenderlyRpcClient.js +60 -0
  155. package/dist/backend/backend/services/onchain/tenderlyRpcClient.js.map +1 -0
  156. package/dist/backend/backend/services/onchain/types.js +14 -0
  157. package/dist/backend/backend/services/onchain/types.js.map +1 -0
  158. package/dist/backend/backend/services/onchainAutomationService.js +316 -0
  159. package/dist/backend/backend/services/onchainAutomationService.js.map +1 -0
  160. package/dist/backend/backend/services/robinhood/auth.js +26 -0
  161. package/dist/backend/backend/services/robinhood/auth.js.map +1 -0
  162. package/dist/backend/backend/services/robinhood/client.js +73 -0
  163. package/dist/backend/backend/services/robinhood/client.js.map +1 -0
  164. package/dist/backend/backend/services/robinhood/integrationService.js +119 -0
  165. package/dist/backend/backend/services/robinhood/integrationService.js.map +1 -0
  166. package/dist/backend/backend/services/robinhood/provider.js +17 -0
  167. package/dist/backend/backend/services/robinhood/provider.js.map +1 -0
  168. package/dist/backend/backend/services/robinhood/unofficialClient.js +61 -0
  169. package/dist/backend/backend/services/robinhood/unofficialClient.js.map +1 -0
  170. package/dist/backend/backend/services/rpcService.js +48 -0
  171. package/dist/backend/backend/services/rpcService.js.map +1 -0
  172. package/dist/backend/backend/services/runtimeTelemetryService.js +96 -0
  173. package/dist/backend/backend/services/runtimeTelemetryService.js.map +1 -0
  174. package/dist/backend/backend/services/safeConfigRepository.js +147 -0
  175. package/dist/backend/backend/services/safeConfigRepository.js.map +1 -0
  176. package/dist/backend/backend/services/safeService.js +527 -0
  177. package/dist/backend/backend/services/safeService.js.map +1 -0
  178. package/dist/backend/backend/services/sandboxService.js +135 -0
  179. package/dist/backend/backend/services/sandboxService.js.map +1 -0
  180. package/dist/backend/backend/services/secureSettingsService.js +50 -0
  181. package/dist/backend/backend/services/secureSettingsService.js.map +1 -0
  182. package/dist/backend/backend/services/transactionHoldService.js +184 -0
  183. package/dist/backend/backend/services/transactionHoldService.js.map +1 -0
  184. package/dist/backend/backend/services/vanityService.js +235 -0
  185. package/dist/backend/backend/services/vanityService.js.map +1 -0
  186. package/dist/backend/backend/services/walletService.js +202 -0
  187. package/dist/backend/backend/services/walletService.js.map +1 -0
  188. package/dist/backend/backend/services/walletStore.js +132 -0
  189. package/dist/backend/backend/services/walletStore.js.map +1 -0
  190. package/dist/backend/backend/utils/abiResolver.js +182 -0
  191. package/dist/backend/backend/utils/abiResolver.js.map +1 -0
  192. package/dist/backend/backend/utils/http.js +12 -0
  193. package/dist/backend/backend/utils/http.js.map +1 -0
  194. package/dist/backend/backend/utils/secretsResolver.js +137 -0
  195. package/dist/backend/backend/utils/secretsResolver.js.map +1 -0
  196. package/dist/backend/backend/utils/signer.js +15 -0
  197. package/dist/backend/backend/utils/signer.js.map +1 -0
  198. package/dist/backend/backend/workers/vanityWorker.js +63 -0
  199. package/dist/backend/backend/workers/vanityWorker.js.map +1 -0
  200. package/dist/backend/cli/gnoman.js +387 -0
  201. package/dist/backend/cli/gnoman.js.map +1 -0
  202. package/dist/backend/modules/sandbox/abiLoader.js +78 -0
  203. package/dist/backend/modules/sandbox/abiLoader.js.map +1 -0
  204. package/dist/backend/modules/sandbox/contractSimulator.js +205 -0
  205. package/dist/backend/modules/sandbox/contractSimulator.js.map +1 -0
  206. package/dist/backend/modules/sandbox/formBuilder.js +14 -0
  207. package/dist/backend/modules/sandbox/formBuilder.js.map +1 -0
  208. package/dist/backend/modules/sandbox/index.js +24 -0
  209. package/dist/backend/modules/sandbox/index.js.map +1 -0
  210. package/dist/backend/modules/sandbox/localFork.js +103 -0
  211. package/dist/backend/modules/sandbox/localFork.js.map +1 -0
  212. package/dist/backend/modules/sandbox/sandboxManager.js +130 -0
  213. package/dist/backend/modules/sandbox/sandboxManager.js.map +1 -0
  214. package/dist/backend/modules/sandbox/types.js +3 -0
  215. package/dist/backend/modules/sandbox/types.js.map +1 -0
  216. package/dist/backend/src/core/backends/fileBackend.js +136 -0
  217. package/dist/backend/src/core/backends/fileBackend.js.map +1 -0
  218. package/dist/backend/src/core/backends/memoryBackend.js +26 -0
  219. package/dist/backend/src/core/backends/memoryBackend.js.map +1 -0
  220. package/dist/backend/src/core/backends/systemBackend.js +86 -0
  221. package/dist/backend/src/core/backends/systemBackend.js.map +1 -0
  222. package/dist/backend/src/core/backends/types.js +12 -0
  223. package/dist/backend/src/core/backends/types.js.map +1 -0
  224. package/dist/backend/src/core/keyringManager.js +178 -0
  225. package/dist/backend/src/core/keyringManager.js.map +1 -0
  226. package/dist/backend/src/utils/abiResolver.js +180 -0
  227. package/dist/backend/src/utils/abiResolver.js.map +1 -0
  228. package/dist/backend/src/utils/runtimeObservability.js +78 -0
  229. package/dist/backend/src/utils/runtimeObservability.js.map +1 -0
  230. package/dist/backend/src/utils/secretsResolver.js +138 -0
  231. package/dist/backend/src/utils/secretsResolver.js.map +1 -0
  232. package/dist/cli/backend/services/diagnosticsService.js +286 -0
  233. package/dist/cli/backend/services/diagnosticsService.js.map +1 -0
  234. package/dist/cli/backend/services/keyringAccessor.js +40 -0
  235. package/dist/cli/backend/services/keyringAccessor.js.map +1 -0
  236. package/dist/cli/backend/services/rpcService.js +48 -0
  237. package/dist/cli/backend/services/rpcService.js.map +1 -0
  238. package/dist/cli/backend/services/runtimeTelemetryService.js +96 -0
  239. package/dist/cli/backend/services/runtimeTelemetryService.js.map +1 -0
  240. package/dist/cli/backend/services/walletService.js +202 -0
  241. package/dist/cli/backend/services/walletService.js.map +1 -0
  242. package/dist/cli/backend/services/walletStore.js +132 -0
  243. package/dist/cli/backend/services/walletStore.js.map +1 -0
  244. package/dist/cli/backend/utils/http.js +12 -0
  245. package/dist/cli/backend/utils/http.js.map +1 -0
  246. package/dist/cli/backend/utils/secretsResolver.js +137 -0
  247. package/dist/cli/backend/utils/secretsResolver.js.map +1 -0
  248. package/dist/cli/cli/gnoman.js +387 -0
  249. package/dist/cli/cli/gnoman.js.map +1 -0
  250. package/dist/cli/src/core/backends/fileBackend.js +136 -0
  251. package/dist/cli/src/core/backends/fileBackend.js.map +1 -0
  252. package/dist/cli/src/core/backends/memoryBackend.js +26 -0
  253. package/dist/cli/src/core/backends/memoryBackend.js.map +1 -0
  254. package/dist/cli/src/core/backends/systemBackend.js +86 -0
  255. package/dist/cli/src/core/backends/systemBackend.js.map +1 -0
  256. package/dist/cli/src/core/backends/types.js +12 -0
  257. package/dist/cli/src/core/backends/types.js.map +1 -0
  258. package/dist/cli/src/core/keyringManager.js +178 -0
  259. package/dist/cli/src/core/keyringManager.js.map +1 -0
  260. package/dist/cli/src/utils/abiResolver.js +180 -0
  261. package/dist/cli/src/utils/abiResolver.js.map +1 -0
  262. package/dist/cli/src/utils/runtimeObservability.js +78 -0
  263. package/dist/cli/src/utils/runtimeObservability.js.map +1 -0
  264. package/dist/cli/src/utils/secretsResolver.js +138 -0
  265. package/dist/cli/src/utils/secretsResolver.js.map +1 -0
  266. package/dist/main/backend/services/keyringAccessor.js +40 -0
  267. package/dist/main/backend/services/keyringAccessor.js.map +1 -0
  268. package/dist/main/backend/utils/http.js +12 -0
  269. package/dist/main/backend/utils/http.js.map +1 -0
  270. package/dist/main/main/ipcHandlers/index.js +26 -0
  271. package/dist/main/main/ipcHandlers/index.js.map +1 -0
  272. package/dist/main/main/keyring/keyringmanager.js +101 -0
  273. package/dist/main/main/keyring/keyringmanager.js.map +1 -0
  274. package/dist/main/main/main.js +224 -0
  275. package/dist/main/main/main.js.map +1 -0
  276. package/dist/main/main/preload/index.js +19 -0
  277. package/dist/main/main/preload/index.js.map +1 -0
  278. package/dist/main/main/preload/licenseBridge.js +105 -0
  279. package/dist/main/main/preload/licenseBridge.js.map +1 -0
  280. package/dist/main/src/core/backends/fileBackend.js +136 -0
  281. package/dist/main/src/core/backends/fileBackend.js.map +1 -0
  282. package/dist/main/src/core/backends/memoryBackend.js +26 -0
  283. package/dist/main/src/core/backends/memoryBackend.js.map +1 -0
  284. package/dist/main/src/core/backends/systemBackend.js +86 -0
  285. package/dist/main/src/core/backends/systemBackend.js.map +1 -0
  286. package/dist/main/src/core/backends/types.js +12 -0
  287. package/dist/main/src/core/backends/types.js.map +1 -0
  288. package/dist/main/src/core/keyringManager.js +178 -0
  289. package/dist/main/src/core/keyringManager.js.map +1 -0
  290. package/dist/main/src/utils/abiResolver.js +180 -0
  291. package/dist/main/src/utils/abiResolver.js.map +1 -0
  292. package/dist/main/src/utils/runtimeObservability.js +78 -0
  293. package/dist/main/src/utils/runtimeObservability.js.map +1 -0
  294. package/dist/main/src/utils/secretsResolver.js +138 -0
  295. package/dist/main/src/utils/secretsResolver.js.map +1 -0
  296. package/docs/development-guide.md +203 -0
  297. package/docs/etherscan-chainlink-integration.md +44 -0
  298. package/docs/gnoman-20-user-manual-STANDARD-PRINT-READY.pdf +0 -0
  299. package/docs/gnoman-20-user-manual-STANDARD.pdf +0 -0
  300. package/docs/license-dev-guide.md +106 -0
  301. package/docs/robinhood-integration.md +30 -0
  302. package/docs/system-audit-gpt-guide.md +208 -0
  303. package/docs/system-robustness-audit.md +50 -0
  304. package/docs/user-guide.md +73 -0
  305. package/docs/wiki/development-guide.md +203 -0
  306. package/docs/wiki/license-dev-guide.md +106 -0
  307. package/docs/wiki/user-guide.md +73 -0
  308. package/eslint.config.js +85 -0
  309. package/gnoman2.0/.eslintrc.cjs +24 -0
  310. package/gnoman2.0/.prettierrc.json +6 -0
  311. package/gnoman2.0/CODex_TASKS.md +124 -0
  312. package/gnoman2.0/LICENSE.md +164 -0
  313. package/gnoman2.0/README.md +95 -0
  314. package/gnoman2.0/assets/GnoLogo.jpg +0 -0
  315. package/gnoman2.0/assets/self.png +0 -0
  316. package/gnoman2.0/backend/controllers/contractController.ts +49 -0
  317. package/gnoman2.0/backend/controllers/devToolsController.ts +76 -0
  318. package/gnoman2.0/backend/controllers/etherscanController.ts +59 -0
  319. package/gnoman2.0/backend/controllers/historyController.ts +7 -0
  320. package/gnoman2.0/backend/controllers/keyringController.ts +134 -0
  321. package/gnoman2.0/backend/controllers/robinhoodController.ts +80 -0
  322. package/gnoman2.0/backend/controllers/safeController.ts +167 -0
  323. package/gnoman2.0/backend/controllers/sandboxController.ts +63 -0
  324. package/gnoman2.0/backend/controllers/settingsController.ts +38 -0
  325. package/gnoman2.0/backend/controllers/walletController.ts +151 -0
  326. package/gnoman2.0/backend/index.ts +133 -0
  327. package/gnoman2.0/backend/licenses/license_public.pem +3 -0
  328. package/gnoman2.0/backend/licenses/verify_license.py +43 -0
  329. package/gnoman2.0/backend/routes/contractRoutes.ts +11 -0
  330. package/gnoman2.0/backend/routes/devToolsRoutes.ts +11 -0
  331. package/gnoman2.0/backend/routes/etherscanRoutes.ts +11 -0
  332. package/gnoman2.0/backend/routes/historyRoutes.ts +8 -0
  333. package/gnoman2.0/backend/routes/keyringRoutes.ts +25 -0
  334. package/gnoman2.0/backend/routes/licenseRoutes.ts +35 -0
  335. package/gnoman2.0/backend/routes/robinhoodRoutes.ts +22 -0
  336. package/gnoman2.0/backend/routes/runtimeRoutes.ts +29 -0
  337. package/gnoman2.0/backend/routes/safeRoutes.ts +28 -0
  338. package/gnoman2.0/backend/routes/sandboxRoutes.ts +17 -0
  339. package/gnoman2.0/backend/routes/settingsRoutes.ts +14 -0
  340. package/gnoman2.0/backend/routes/walletRoutes.ts +21 -0
  341. package/gnoman2.0/backend/services/chainlinkService.ts +65 -0
  342. package/gnoman2.0/backend/services/contractRegistryService.ts +205 -0
  343. package/gnoman2.0/backend/services/devToolsService.ts +251 -0
  344. package/gnoman2.0/backend/services/diagnosticsService.ts +350 -0
  345. package/gnoman2.0/backend/services/etherscanService.ts +152 -0
  346. package/gnoman2.0/backend/services/historyService.ts +89 -0
  347. package/gnoman2.0/backend/services/keyringAccessor.ts +4 -0
  348. package/gnoman2.0/backend/services/licenseService.ts +163 -0
  349. package/gnoman2.0/backend/services/onchain/abiRegistry.ts +57 -0
  350. package/gnoman2.0/backend/services/onchain/chainlinkClient.ts +56 -0
  351. package/gnoman2.0/backend/services/onchain/errors.ts +16 -0
  352. package/gnoman2.0/backend/services/onchain/etherscanClient.ts +94 -0
  353. package/gnoman2.0/backend/services/onchain/index.ts +76 -0
  354. package/gnoman2.0/backend/services/onchain/tenderlyRpcClient.ts +74 -0
  355. package/gnoman2.0/backend/services/onchain/types.ts +33 -0
  356. package/gnoman2.0/backend/services/onchainAutomationService.ts +424 -0
  357. package/gnoman2.0/backend/services/robinhood/auth.ts +42 -0
  358. package/gnoman2.0/backend/services/robinhood/client.ts +123 -0
  359. package/gnoman2.0/backend/services/robinhood/integrationService.ts +140 -0
  360. package/gnoman2.0/backend/services/robinhood/provider.ts +22 -0
  361. package/gnoman2.0/backend/services/robinhood/unofficialClient.ts +66 -0
  362. package/gnoman2.0/backend/services/rpcService.ts +44 -0
  363. package/gnoman2.0/backend/services/runtimeTelemetryService.ts +158 -0
  364. package/gnoman2.0/backend/services/safeConfigRepository.ts +205 -0
  365. package/gnoman2.0/backend/services/safeService.ts +588 -0
  366. package/gnoman2.0/backend/services/sandboxService.ts +157 -0
  367. package/gnoman2.0/backend/services/secureSettingsService.ts +45 -0
  368. package/gnoman2.0/backend/services/transactionHoldService.ts +223 -0
  369. package/gnoman2.0/backend/services/vanityService.ts +293 -0
  370. package/gnoman2.0/backend/services/walletService.ts +290 -0
  371. package/gnoman2.0/backend/services/walletStore.ts +179 -0
  372. package/gnoman2.0/backend/types/express-async-handler.d.ts +13 -0
  373. package/gnoman2.0/backend/types/keyring.d.ts +19 -0
  374. package/gnoman2.0/backend/utils/abiResolver.ts +208 -0
  375. package/gnoman2.0/backend/utils/http.ts +6 -0
  376. package/gnoman2.0/backend/utils/secretsResolver.ts +150 -0
  377. package/gnoman2.0/backend/utils/signer.ts +11 -0
  378. package/gnoman2.0/backend/workers/vanityWorker.ts +76 -0
  379. package/gnoman2.0/capacitor.config.ts +13 -0
  380. package/gnoman2.0/cli/gnoman.ts +424 -0
  381. package/gnoman2.0/contracts/OracleConsumer.sol +20 -0
  382. package/gnoman2.0/contracts/PriceFeedConsumer.sol +22 -0
  383. package/gnoman2.0/docs/development-guide.md +203 -0
  384. package/gnoman2.0/docs/etherscan-chainlink-integration.md +44 -0
  385. package/gnoman2.0/docs/gnoman-20-user-manual-STANDARD-PRINT-READY.pdf +0 -0
  386. package/gnoman2.0/docs/gnoman-20-user-manual-STANDARD.pdf +0 -0
  387. package/gnoman2.0/docs/license-dev-guide.md +106 -0
  388. package/gnoman2.0/docs/robinhood-integration.md +30 -0
  389. package/gnoman2.0/docs/system-audit-gpt-guide.md +208 -0
  390. package/gnoman2.0/docs/system-robustness-audit.md +50 -0
  391. package/gnoman2.0/docs/user-guide.md +73 -0
  392. package/gnoman2.0/docs/wiki/development-guide.md +203 -0
  393. package/gnoman2.0/docs/wiki/license-dev-guide.md +106 -0
  394. package/gnoman2.0/docs/wiki/user-guide.md +73 -0
  395. package/gnoman2.0/eslint.config.js +85 -0
  396. package/gnoman2.0/gnomon/__init__.py +0 -0
  397. package/gnoman2.0/gnomon/api/__init__.py +0 -0
  398. package/gnoman2.0/gnomon/api/etherscan_tracker.py +72 -0
  399. package/gnoman2.0/gnomon/core/__init__.py +0 -0
  400. package/gnoman2.0/gnomon/core/safe_manager.py +111 -0
  401. package/gnoman2.0/gnomon/tests/test_abi_resolver.py +181 -0
  402. package/gnoman2.0/gnomon/tests/test_safe_persistence_and_etherscan.py +97 -0
  403. package/gnoman2.0/gnomon/utils/__init__.py +5 -0
  404. package/gnoman2.0/gnomon/utils/abi_resolver.py +255 -0
  405. package/gnoman2.0/ios/ExportOptions.plist +16 -0
  406. package/gnoman2.0/ios/README.md +33 -0
  407. package/gnoman2.0/jest.config.ts +18 -0
  408. package/gnoman2.0/keyring/__init__.py +17 -0
  409. package/gnoman2.0/licensingServer/package.json +23 -0
  410. package/gnoman2.0/licensingServer/src/config/keys.ts +84 -0
  411. package/gnoman2.0/licensingServer/src/index.ts +30 -0
  412. package/gnoman2.0/licensingServer/src/lib/canonicalize.ts +5 -0
  413. package/gnoman2.0/licensingServer/src/lib/crypto.ts +25 -0
  414. package/gnoman2.0/licensingServer/src/lib/validate.ts +62 -0
  415. package/gnoman2.0/licensingServer/src/middleware/auth.ts +20 -0
  416. package/gnoman2.0/licensingServer/src/routes/licenses.ts +110 -0
  417. package/gnoman2.0/licensingServer/tsconfig.json +12 -0
  418. package/gnoman2.0/main/ipcHandlers/index.ts +23 -0
  419. package/gnoman2.0/main/keyring/keyringmanager.ts +154 -0
  420. package/gnoman2.0/main/main.ts +234 -0
  421. package/gnoman2.0/main/preload/index.ts +31 -0
  422. package/gnoman2.0/main/preload/licenseBridge.ts +73 -0
  423. package/gnoman2.0/modules/sandbox/abiLoader.ts +78 -0
  424. package/gnoman2.0/modules/sandbox/contractSimulator.ts +241 -0
  425. package/gnoman2.0/modules/sandbox/formBuilder.ts +16 -0
  426. package/gnoman2.0/modules/sandbox/index.ts +6 -0
  427. package/gnoman2.0/modules/sandbox/localFork.ts +129 -0
  428. package/gnoman2.0/modules/sandbox/safe.abi.json +82 -0
  429. package/gnoman2.0/modules/sandbox/sandboxManager.ts +154 -0
  430. package/gnoman2.0/modules/sandbox/types.ts +84 -0
  431. package/gnoman2.0/modules/sandbox/ui/LogViewer.tsx +30 -0
  432. package/gnoman2.0/modules/sandbox/ui/ParameterForm.tsx +49 -0
  433. package/gnoman2.0/modules/sandbox/ui/SandboxPanel.tsx +568 -0
  434. package/gnoman2.0/package-lock.json +10904 -0
  435. package/gnoman2.0/package.json +82 -0
  436. package/gnoman2.0/renderer/components/LicenseScreen.tsx +134 -0
  437. package/gnoman2.0/renderer/index.html +12 -0
  438. package/gnoman2.0/renderer/package-lock.json +4104 -0
  439. package/gnoman2.0/renderer/package.json +35 -0
  440. package/gnoman2.0/renderer/postcss.config.cjs +6 -0
  441. package/gnoman2.0/renderer/src/App.tsx +229 -0
  442. package/gnoman2.0/renderer/src/context/KeyringContext.tsx +217 -0
  443. package/gnoman2.0/renderer/src/context/SafeContext.tsx +49 -0
  444. package/gnoman2.0/renderer/src/context/ThemeContext.tsx +60 -0
  445. package/gnoman2.0/renderer/src/context/WalletContext.tsx +50 -0
  446. package/gnoman2.0/renderer/src/context/main.tsx +18 -0
  447. package/gnoman2.0/renderer/src/main.tsx +18 -0
  448. package/gnoman2.0/renderer/src/pages/Contracts.tsx +482 -0
  449. package/gnoman2.0/renderer/src/pages/Dashboard.tsx +653 -0
  450. package/gnoman2.0/renderer/src/pages/DeveloperTools.tsx +270 -0
  451. package/gnoman2.0/renderer/src/pages/History.tsx +149 -0
  452. package/gnoman2.0/renderer/src/pages/Keyring.tsx +449 -0
  453. package/gnoman2.0/renderer/src/pages/Safes.tsx +1089 -0
  454. package/gnoman2.0/renderer/src/pages/Sandbox.tsx +146 -0
  455. package/gnoman2.0/renderer/src/pages/Settings.tsx +871 -0
  456. package/gnoman2.0/renderer/src/pages/Wallets.tsx +752 -0
  457. package/gnoman2.0/renderer/src/pages/WikiGuide.tsx +75 -0
  458. package/gnoman2.0/renderer/src/styles.css +32 -0
  459. package/gnoman2.0/renderer/src/types/gnoman.d.ts +9 -0
  460. package/gnoman2.0/renderer/src/types/license.ts +8 -0
  461. package/gnoman2.0/renderer/src/types/safevault.d.ts +17 -0
  462. package/gnoman2.0/renderer/src/utils/backend.ts +88 -0
  463. package/gnoman2.0/renderer/tailwind.config.cjs +8 -0
  464. package/gnoman2.0/renderer/tsconfig.json +13 -0
  465. package/gnoman2.0/renderer/tsconfig.node.json +9 -0
  466. package/gnoman2.0/renderer/vite.config.ts +19 -0
  467. package/gnoman2.0/requests/__init__.py +35 -0
  468. package/gnoman2.0/scripts/build-ios.sh +30 -0
  469. package/gnoman2.0/scripts/copyBackendAssets.js +24 -0
  470. package/gnoman2.0/scripts/copyRenderer.js +87 -0
  471. package/gnoman2.0/scripts/launchElectron.js +51 -0
  472. package/gnoman2.0/src/core/backends/fileBackend.ts +154 -0
  473. package/gnoman2.0/src/core/backends/memoryBackend.ts +27 -0
  474. package/gnoman2.0/src/core/backends/systemBackend.ts +66 -0
  475. package/gnoman2.0/src/core/backends/types.ts +17 -0
  476. package/gnoman2.0/src/core/keyringManager.ts +208 -0
  477. package/gnoman2.0/src/utils/abiCache/.gitkeep +0 -0
  478. package/gnoman2.0/src/utils/abiResolver.ts +200 -0
  479. package/gnoman2.0/src/utils/runtimeObservability.ts +110 -0
  480. package/gnoman2.0/src/utils/secretsResolver.ts +144 -0
  481. package/gnoman2.0/tests/chainlinkService.test.ts +32 -0
  482. package/gnoman2.0/tests/diagnosticsService.test.ts +68 -0
  483. package/gnoman2.0/tests/etherscanController.test.ts +99 -0
  484. package/gnoman2.0/tests/etherscanService.test.ts +116 -0
  485. package/gnoman2.0/tests/keyringManager.test.ts +135 -0
  486. package/gnoman2.0/tests/onchainToolkit.test.ts +71 -0
  487. package/gnoman2.0/tests/robinhoodClient.test.ts +54 -0
  488. package/gnoman2.0/tests/robinhoodController.test.ts +81 -0
  489. package/gnoman2.0/tests/robinhoodIntegrationService.test.ts +50 -0
  490. package/gnoman2.0/tests/safeServicePersistence.test.ts +81 -0
  491. package/gnoman2.0/tests/test_contract_sandbox/sandbox.test.js +407 -0
  492. package/gnoman2.0/tests/walletController.test.ts +57 -0
  493. package/gnoman2.0/tsconfig.backend.json +7 -0
  494. package/gnoman2.0/tsconfig.cli.json +7 -0
  495. package/gnoman2.0/tsconfig.json +18 -0
  496. package/gnoman2.0/tsconfig.main.json +7 -0
  497. package/gnomon/__init__.py +0 -0
  498. package/gnomon/__pycache__/__init__.cpython-310.pyc +0 -0
  499. package/gnomon/api/__init__.py +0 -0
  500. package/gnomon/api/__pycache__/__init__.cpython-310.pyc +0 -0
  501. package/gnomon/api/__pycache__/etherscan_tracker.cpython-310.pyc +0 -0
  502. package/gnomon/api/etherscan_tracker.py +72 -0
  503. package/gnomon/core/__init__.py +0 -0
  504. package/gnomon/core/safe_manager.py +111 -0
  505. package/gnomon/tests/__pycache__/test_safe_persistence_and_etherscan.cpython-310-pytest-8.3.3.pyc +0 -0
  506. package/gnomon/tests/test_abi_resolver.py +181 -0
  507. package/gnomon/tests/test_safe_persistence_and_etherscan.py +97 -0
  508. package/gnomon/utils/__init__.py +5 -0
  509. package/gnomon/utils/abi_resolver.py +255 -0
  510. package/ios/ExportOptions.plist +16 -0
  511. package/ios/README.md +33 -0
  512. package/jest.config.ts +18 -0
  513. package/keyring/__init__.py +17 -0
  514. package/launcher.sh +57 -0
  515. package/license.env +2 -0
  516. package/licensingServer/package.json +23 -0
  517. package/licensingServer/src/config/keys.ts +84 -0
  518. package/licensingServer/src/index.ts +30 -0
  519. package/licensingServer/src/lib/canonicalize.ts +5 -0
  520. package/licensingServer/src/lib/crypto.ts +25 -0
  521. package/licensingServer/src/lib/validate.ts +62 -0
  522. package/licensingServer/src/middleware/auth.ts +20 -0
  523. package/licensingServer/src/routes/licenses.ts +110 -0
  524. package/licensingServer/tsconfig.json +12 -0
  525. package/main/ipcHandlers/index.ts +23 -0
  526. package/main/keyring/keyringmanager.ts +154 -0
  527. package/main/main.ts +234 -0
  528. package/main/preload/index.ts +31 -0
  529. package/main/preload/licenseBridge.ts +73 -0
  530. package/modules/sandbox/abiLoader.ts +78 -0
  531. package/modules/sandbox/contractSimulator.ts +241 -0
  532. package/modules/sandbox/formBuilder.ts +16 -0
  533. package/modules/sandbox/index.ts +6 -0
  534. package/modules/sandbox/localFork.ts +129 -0
  535. package/modules/sandbox/safe.abi.json +82 -0
  536. package/modules/sandbox/sandboxManager.ts +154 -0
  537. package/modules/sandbox/types.ts +84 -0
  538. package/modules/sandbox/ui/LogViewer.tsx +30 -0
  539. package/modules/sandbox/ui/ParameterForm.tsx +49 -0
  540. package/modules/sandbox/ui/SandboxPanel.tsx +568 -0
  541. package/package.json +82 -0
  542. package/renderer/components/LicenseScreen.tsx +134 -0
  543. package/renderer/index.html +12 -0
  544. package/renderer/package-lock.json +4104 -0
  545. package/renderer/package.json +35 -0
  546. package/renderer/postcss.config.cjs +6 -0
  547. package/renderer/src/App.tsx +229 -0
  548. package/renderer/src/context/KeyringContext.tsx +217 -0
  549. package/renderer/src/context/SafeContext.tsx +49 -0
  550. package/renderer/src/context/ThemeContext.tsx +60 -0
  551. package/renderer/src/context/WalletContext.tsx +50 -0
  552. package/renderer/src/context/main.tsx +18 -0
  553. package/renderer/src/main.tsx +18 -0
  554. package/renderer/src/pages/Contracts.tsx +482 -0
  555. package/renderer/src/pages/Dashboard.tsx +653 -0
  556. package/renderer/src/pages/DeveloperTools.tsx +270 -0
  557. package/renderer/src/pages/History.tsx +149 -0
  558. package/renderer/src/pages/Keyring.tsx +449 -0
  559. package/renderer/src/pages/Safes.tsx +1089 -0
  560. package/renderer/src/pages/Sandbox.tsx +146 -0
  561. package/renderer/src/pages/Settings.tsx +871 -0
  562. package/renderer/src/pages/Wallets.tsx +752 -0
  563. package/renderer/src/pages/WikiGuide.tsx +75 -0
  564. package/renderer/src/styles.css +32 -0
  565. package/renderer/src/types/gnoman.d.ts +9 -0
  566. package/renderer/src/types/license.ts +8 -0
  567. package/renderer/src/types/safevault.d.ts +17 -0
  568. package/renderer/src/utils/backend.ts +88 -0
  569. package/renderer/tailwind.config.cjs +8 -0
  570. package/renderer/tsconfig.json +13 -0
  571. package/renderer/tsconfig.node.json +9 -0
  572. package/renderer/vite.config.ts +19 -0
  573. package/requests/__init__.py +35 -0
  574. package/requests/__pycache__/__init__.cpython-310.pyc +0 -0
  575. package/scripts/build-ios.sh +30 -0
  576. package/scripts/copyBackendAssets.js +24 -0
  577. package/scripts/copyRenderer.js +87 -0
  578. package/scripts/deployBackend.sh +24 -0
  579. package/scripts/launchElectron.js +51 -0
  580. package/src/core/backends/fileBackend.ts +154 -0
  581. package/src/core/backends/memoryBackend.ts +27 -0
  582. package/src/core/backends/systemBackend.ts +66 -0
  583. package/src/core/backends/types.ts +17 -0
  584. package/src/core/keyringManager.ts +208 -0
  585. package/src/utils/abiCache/.gitkeep +0 -0
  586. package/src/utils/abiResolver.ts +200 -0
  587. package/src/utils/runtimeObservability.ts +110 -0
  588. package/src/utils/secretsResolver.ts +144 -0
  589. package/tests/chainlinkService.test.ts +32 -0
  590. package/tests/diagnosticsService.test.ts +68 -0
  591. package/tests/etherscanController.test.ts +99 -0
  592. package/tests/etherscanService.test.ts +116 -0
  593. package/tests/keyringManager.test.ts +135 -0
  594. package/tests/onchainToolkit.test.ts +71 -0
  595. package/tests/robinhoodClient.test.ts +54 -0
  596. package/tests/robinhoodController.test.ts +81 -0
  597. package/tests/robinhoodIntegrationService.test.ts +50 -0
  598. package/tests/safeServicePersistence.test.ts +81 -0
  599. package/tests/test_contract_sandbox/sandbox.test.js +407 -0
  600. package/tests/walletController.test.ts +57 -0
  601. package/touch +14 -0
  602. package/tsconfig.backend.json +7 -0
  603. package/tsconfig.cli.json +7 -0
  604. package/tsconfig.json +18 -0
  605. package/tsconfig.main.json +7 -0
  606. package/webhook-shim.js +50 -0
@@ -0,0 +1,1089 @@
1
+ import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useSafe, type SafeState, type SafeDelegate } from '../context/SafeContext';
3
+ import { buildBackendUrl } from '../utils/backend';
4
+
5
+ interface HoldRecord {
6
+ txHash: string;
7
+ safeAddress: string;
8
+ createdAt: string;
9
+ holdUntil: string;
10
+ executed: number;
11
+ holdHours: number;
12
+ }
13
+
14
+ interface HoldSummary {
15
+ executed: number;
16
+ pending: number;
17
+ }
18
+
19
+ interface EffectivePolicy {
20
+ global: { enabled: boolean; holdHours: number };
21
+ local: { enabled: boolean; holdHours: number; updatedAt: string; safeAddress: string };
22
+ }
23
+
24
+ interface SafeDetails {
25
+ address: string;
26
+ threshold: number;
27
+ owners: string[];
28
+ delegates: SafeDelegate[];
29
+ modules: string[];
30
+ fallbackHandler?: string;
31
+ guard?: string;
32
+ rpcUrl: string;
33
+ network?: string;
34
+ balance?: string;
35
+ holdPolicy: { enabled: boolean; holdHours: number; updatedAt: string };
36
+ holdSummary: HoldSummary;
37
+ effectiveHold: EffectivePolicy;
38
+ }
39
+
40
+ const formatPolicyUpdatedAt = (timestamp?: string) => {
41
+ if (!timestamp) {
42
+ return 'Not configured';
43
+ }
44
+ const parsed = new Date(timestamp);
45
+ if (Number.isNaN(parsed.getTime()) || parsed.getTime() <= 0) {
46
+ return 'Not configured';
47
+ }
48
+ return parsed.toLocaleString();
49
+ };
50
+
51
+ const Safes = () => {
52
+ const { currentSafe, setCurrentSafe } = useSafe();
53
+ const [loading, setLoading] = useState(false);
54
+ const [error, setError] = useState<string | undefined>();
55
+ const [heldTransactions, setHeldTransactions] = useState<HoldRecord[]>([]);
56
+ const [holdSummary, setHoldSummary] = useState<HoldSummary>({ executed: 0, pending: 0 });
57
+ const [holdPolicy, setHoldPolicy] = useState<EffectivePolicy>();
58
+ const [holdForm, setHoldForm] = useState({ enabled: true, holdHours: 24 });
59
+ const [holdSaving, setHoldSaving] = useState(false);
60
+ const [holdMessage, setHoldMessage] = useState<string>();
61
+ const [, setTick] = useState(0);
62
+ const [detailsOpen, setDetailsOpen] = useState(false);
63
+ const [detailsLoading, setDetailsLoading] = useState(false);
64
+ const [detailsError, setDetailsError] = useState<string>();
65
+ const [details, setDetails] = useState<SafeDetails>();
66
+ const [ownerForm, setOwnerForm] = useState({ address: '', threshold: 1 });
67
+ const [ownerRemoveForm, setOwnerRemoveForm] = useState({ address: '', threshold: 1 });
68
+ const [thresholdForm, setThresholdForm] = useState(1);
69
+ const [moduleForm, setModuleForm] = useState('');
70
+ const [delegateForm, setDelegateForm] = useState({ address: '', label: '' });
71
+ const [fallbackForm, setFallbackForm] = useState('');
72
+ const [guardForm, setGuardForm] = useState('');
73
+ const [actionMessage, setActionMessage] = useState<string>();
74
+ const [actionError, setActionError] = useState<string>();
75
+ const [txForm, setTxForm] = useState({ to: '', value: '', data: '' });
76
+ const [txLoading, setTxLoading] = useState(false);
77
+ const [txMessage, setTxMessage] = useState<string>();
78
+ const [txError, setTxError] = useState<string>();
79
+
80
+ const refreshSafe = useCallback(
81
+ async (safeAddress: string) => {
82
+ const [detailsResponse, heldResponse] = await Promise.all([
83
+ fetch(buildBackendUrl(`/api/safes/${safeAddress}/details`)),
84
+ fetch(buildBackendUrl(`/api/safes/${safeAddress}/transactions/held`))
85
+ ]);
86
+ if (!detailsResponse.ok) {
87
+ throw new Error('Failed to load Safe details');
88
+ }
89
+ const safeDetails = (await detailsResponse.json()) as SafeDetails;
90
+ const heldPayload = heldResponse.ok ? await heldResponse.json() : [];
91
+ const records = Array.isArray(heldPayload)
92
+ ? (heldPayload as HoldRecord[])
93
+ : ((heldPayload?.records ?? []) as HoldRecord[]);
94
+ setCurrentSafe((prev) =>
95
+ prev && prev.address === safeAddress
96
+ ? {
97
+ ...prev,
98
+ owners: safeDetails.owners,
99
+ threshold: safeDetails.threshold,
100
+ modules: safeDetails.modules,
101
+ delegates: safeDetails.delegates,
102
+ fallbackHandler: safeDetails.fallbackHandler,
103
+ guard: safeDetails.guard,
104
+ network: safeDetails.network,
105
+ rpcUrl: safeDetails.rpcUrl,
106
+ balance: safeDetails.balance
107
+ }
108
+ : prev
109
+ );
110
+ setHeldTransactions(records);
111
+ if (!Array.isArray(heldPayload) && heldPayload) {
112
+ setHoldSummary(heldPayload.summary ?? { executed: 0, pending: 0 });
113
+ if (heldPayload.effective) {
114
+ const effective = heldPayload.effective as EffectivePolicy;
115
+ setHoldPolicy(effective);
116
+ setHoldForm({
117
+ enabled: effective.local.enabled,
118
+ holdHours: effective.local.holdHours
119
+ });
120
+ }
121
+ }
122
+ },
123
+ [setCurrentSafe]
124
+ );
125
+
126
+ useEffect(() => {
127
+ if (currentSafe) {
128
+ refreshSafe(currentSafe.address).catch((err) => setError(err instanceof Error ? err.message : String(err)));
129
+ }
130
+ }, [currentSafe?.address, refreshSafe]);
131
+
132
+ useEffect(() => {
133
+ const timer = window.setInterval(() => setTick((tick) => tick + 1), 1000);
134
+ return () => {
135
+ window.clearInterval(timer);
136
+ };
137
+ }, []);
138
+
139
+ useEffect(() => {
140
+ if (!currentSafe) {
141
+ return;
142
+ }
143
+ setOwnerForm((prev) => ({ ...prev, threshold: currentSafe.threshold }));
144
+ setOwnerRemoveForm((prev) => ({ ...prev, threshold: currentSafe.threshold }));
145
+ setThresholdForm(currentSafe.threshold);
146
+ setFallbackForm(currentSafe.fallbackHandler ?? '');
147
+ setGuardForm(currentSafe.guard ?? '');
148
+ }, [currentSafe?.threshold, currentSafe?.fallbackHandler, currentSafe?.guard, currentSafe]);
149
+
150
+ const countdowns = useMemo(() => {
151
+ const now = Date.now();
152
+ return heldTransactions.reduce<Record<string, string>>((acc, hold) => {
153
+ const remaining = new Date(hold.holdUntil).getTime() - now;
154
+ if (Number.isNaN(remaining)) {
155
+ acc[hold.txHash] = 'Unknown';
156
+ return acc;
157
+ }
158
+ if (remaining <= 0) {
159
+ acc[hold.txHash] = 'Ready';
160
+ return acc;
161
+ }
162
+ const seconds = Math.floor(remaining / 1000);
163
+ const hours = Math.floor(seconds / 3600);
164
+ const minutes = Math.floor((seconds % 3600) / 60);
165
+ const secs = seconds % 60;
166
+ acc[hold.txHash] = `${hours.toString().padStart(2, '0')}:${minutes
167
+ .toString()
168
+ .padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
169
+ return acc;
170
+ }, {});
171
+ }, [heldTransactions]);
172
+
173
+ const releaseHold = async (txHash: string) => {
174
+ if (!currentSafe) {
175
+ return;
176
+ }
177
+ try {
178
+ const response = await fetch(
179
+ buildBackendUrl(`/api/safes/${currentSafe.address}/transactions/${txHash}/release`),
180
+ {
181
+ method: 'POST'
182
+ }
183
+ );
184
+ if (!response.ok) {
185
+ throw new Error('Failed to release hold');
186
+ }
187
+ await refreshSafe(currentSafe.address);
188
+ } catch (err) {
189
+ setError(err instanceof Error ? err.message : 'Unable to release hold');
190
+ }
191
+ };
192
+
193
+ const handleHoldSubmit = async (event: FormEvent<HTMLFormElement>) => {
194
+ event.preventDefault();
195
+ if (!currentSafe) {
196
+ return;
197
+ }
198
+ setHoldSaving(true);
199
+ setHoldMessage(undefined);
200
+ try {
201
+ const response = await fetch(buildBackendUrl(`/api/safes/${currentSafe.address}/hold`), {
202
+ method: 'POST',
203
+ headers: { 'Content-Type': 'application/json' },
204
+ body: JSON.stringify({ enabled: holdForm.enabled, holdHours: holdForm.holdHours })
205
+ });
206
+ if (!response.ok) {
207
+ throw new Error('Failed to update hold policy');
208
+ }
209
+ const payload = (await response.json()) as {
210
+ policy: EffectivePolicy['local'];
211
+ summary: HoldSummary;
212
+ effective: EffectivePolicy;
213
+ };
214
+ setHoldPolicy(payload.effective);
215
+ setHoldSummary(payload.summary);
216
+ setHoldForm({ enabled: payload.policy.enabled, holdHours: payload.policy.holdHours });
217
+ setHoldMessage('Hold policy saved');
218
+ } catch (err) {
219
+ setHoldMessage(err instanceof Error ? err.message : 'Unable to save hold policy');
220
+ } finally {
221
+ setHoldSaving(false);
222
+ }
223
+ };
224
+
225
+ const handleConnect = async (event: FormEvent<HTMLFormElement>) => {
226
+ event.preventDefault();
227
+ const formData = new FormData(event.currentTarget);
228
+ const address = String(formData.get('address') ?? '');
229
+ setLoading(true);
230
+ setError(undefined);
231
+ try {
232
+ const response = await fetch(buildBackendUrl('/api/safes/load'), {
233
+ method: 'POST',
234
+ headers: { 'Content-Type': 'application/json' },
235
+ body: JSON.stringify({ address })
236
+ });
237
+ if (!response.ok) {
238
+ throw new Error('Failed to load Safe');
239
+ }
240
+ const data = (await response.json()) as SafeState;
241
+ setCurrentSafe(data);
242
+ } catch (err) {
243
+ setError(err instanceof Error ? err.message : 'Unable to connect Safe');
244
+ } finally {
245
+ setLoading(false);
246
+ }
247
+ };
248
+
249
+ const openDetails = async () => {
250
+ if (!currentSafe) {
251
+ return;
252
+ }
253
+ setDetailsOpen(true);
254
+ setDetailsLoading(true);
255
+ setDetailsError(undefined);
256
+ setDetails(undefined);
257
+ try {
258
+ const response = await fetch(buildBackendUrl(`/api/safes/${currentSafe.address}/details`));
259
+ if (!response.ok) {
260
+ throw new Error('Unable to load Safe properties');
261
+ }
262
+ const payload = (await response.json()) as SafeDetails;
263
+ setDetails(payload);
264
+ } catch (err) {
265
+ setDetailsError(err instanceof Error ? err.message : 'Failed to load Safe details');
266
+ } finally {
267
+ setDetailsLoading(false);
268
+ }
269
+ };
270
+
271
+ const closeDetails = () => {
272
+ setDetailsOpen(false);
273
+ setDetails(undefined);
274
+ setDetailsError(undefined);
275
+ };
276
+
277
+ const syncSafe = async () => {
278
+ if (!currentSafe) {
279
+ return;
280
+ }
281
+ setActionMessage(undefined);
282
+ setActionError(undefined);
283
+ try {
284
+ const response = await fetch(buildBackendUrl(`/api/safes/${currentSafe.address}/sync`), {
285
+ method: 'POST'
286
+ });
287
+ if (!response.ok) {
288
+ throw new Error('Failed to sync Safe state');
289
+ }
290
+ const payload = (await response.json()) as SafeState;
291
+ setCurrentSafe(payload);
292
+ setActionMessage('Safe state synchronized');
293
+ } catch (err) {
294
+ setActionError(err instanceof Error ? err.message : 'Unable to sync Safe');
295
+ }
296
+ };
297
+
298
+ const handleAddOwner = async (event: FormEvent<HTMLFormElement>) => {
299
+ event.preventDefault();
300
+ if (!currentSafe) {
301
+ return;
302
+ }
303
+ setActionMessage(undefined);
304
+ setActionError(undefined);
305
+ try {
306
+ const response = await fetch(buildBackendUrl(`/api/safes/${currentSafe.address}/owners`), {
307
+ method: 'POST',
308
+ headers: { 'Content-Type': 'application/json' },
309
+ body: JSON.stringify({ owner: ownerForm.address, threshold: ownerForm.threshold })
310
+ });
311
+ if (!response.ok) {
312
+ throw new Error('Failed to add owner');
313
+ }
314
+ const payload = (await response.json()) as { owners: string[]; threshold: number };
315
+ setCurrentSafe((prev) => (prev ? { ...prev, owners: payload.owners, threshold: payload.threshold } : prev));
316
+ setOwnerForm({ address: '', threshold: payload.threshold });
317
+ setActionMessage('Owner added');
318
+ } catch (err) {
319
+ setActionError(err instanceof Error ? err.message : 'Unable to add owner');
320
+ }
321
+ };
322
+
323
+ const handleRemoveOwner = async (event: FormEvent<HTMLFormElement>) => {
324
+ event.preventDefault();
325
+ if (!currentSafe) {
326
+ return;
327
+ }
328
+ setActionMessage(undefined);
329
+ setActionError(undefined);
330
+ try {
331
+ const response = await fetch(
332
+ buildBackendUrl(`/api/safes/${currentSafe.address}/owners/${ownerRemoveForm.address}`),
333
+ {
334
+ method: 'DELETE',
335
+ headers: { 'Content-Type': 'application/json' },
336
+ body: JSON.stringify({ threshold: ownerRemoveForm.threshold })
337
+ }
338
+ );
339
+ if (!response.ok) {
340
+ throw new Error('Failed to remove owner');
341
+ }
342
+ const payload = (await response.json()) as { owners: string[]; threshold: number };
343
+ setCurrentSafe((prev) => (prev ? { ...prev, owners: payload.owners, threshold: payload.threshold } : prev));
344
+ setOwnerRemoveForm({ address: '', threshold: payload.threshold });
345
+ setActionMessage('Owner removed');
346
+ } catch (err) {
347
+ setActionError(err instanceof Error ? err.message : 'Unable to remove owner');
348
+ }
349
+ };
350
+
351
+ const handleThresholdUpdate = async (event: FormEvent<HTMLFormElement>) => {
352
+ event.preventDefault();
353
+ if (!currentSafe) {
354
+ return;
355
+ }
356
+ setActionMessage(undefined);
357
+ setActionError(undefined);
358
+ try {
359
+ const response = await fetch(buildBackendUrl(`/api/safes/${currentSafe.address}/threshold`), {
360
+ method: 'POST',
361
+ headers: { 'Content-Type': 'application/json' },
362
+ body: JSON.stringify({ threshold: thresholdForm })
363
+ });
364
+ if (!response.ok) {
365
+ throw new Error('Failed to update threshold');
366
+ }
367
+ const payload = (await response.json()) as { threshold: number };
368
+ setCurrentSafe((prev) => (prev ? { ...prev, threshold: payload.threshold } : prev));
369
+ setActionMessage('Threshold updated');
370
+ } catch (err) {
371
+ setActionError(err instanceof Error ? err.message : 'Unable to update threshold');
372
+ }
373
+ };
374
+
375
+ const handleAddModule = async (event: FormEvent<HTMLFormElement>) => {
376
+ event.preventDefault();
377
+ if (!currentSafe) {
378
+ return;
379
+ }
380
+ setActionMessage(undefined);
381
+ setActionError(undefined);
382
+ try {
383
+ const response = await fetch(buildBackendUrl(`/api/safes/${currentSafe.address}/modules`), {
384
+ method: 'POST',
385
+ headers: { 'Content-Type': 'application/json' },
386
+ body: JSON.stringify({ module: moduleForm })
387
+ });
388
+ if (!response.ok) {
389
+ throw new Error('Failed to enable module');
390
+ }
391
+ const payload = (await response.json()) as { modules: string[] };
392
+ setCurrentSafe((prev) => (prev ? { ...prev, modules: payload.modules } : prev));
393
+ setModuleForm('');
394
+ setActionMessage('Module enabled');
395
+ } catch (err) {
396
+ setActionError(err instanceof Error ? err.message : 'Unable to enable module');
397
+ }
398
+ };
399
+
400
+ const handleRemoveModule = async (moduleAddress: string) => {
401
+ if (!currentSafe) {
402
+ return;
403
+ }
404
+ setActionMessage(undefined);
405
+ setActionError(undefined);
406
+ try {
407
+ const response = await fetch(
408
+ buildBackendUrl(`/api/safes/${currentSafe.address}/modules/${moduleAddress}`),
409
+ { method: 'DELETE' }
410
+ );
411
+ if (!response.ok) {
412
+ throw new Error('Failed to disable module');
413
+ }
414
+ const payload = (await response.json()) as { modules: string[] };
415
+ setCurrentSafe((prev) => (prev ? { ...prev, modules: payload.modules } : prev));
416
+ setActionMessage('Module disabled');
417
+ } catch (err) {
418
+ setActionError(err instanceof Error ? err.message : 'Unable to disable module');
419
+ }
420
+ };
421
+
422
+ const handleAddDelegate = async (event: FormEvent<HTMLFormElement>) => {
423
+ event.preventDefault();
424
+ if (!currentSafe) {
425
+ return;
426
+ }
427
+ setActionMessage(undefined);
428
+ setActionError(undefined);
429
+ try {
430
+ const response = await fetch(buildBackendUrl(`/api/safes/${currentSafe.address}/delegates`), {
431
+ method: 'POST',
432
+ headers: { 'Content-Type': 'application/json' },
433
+ body: JSON.stringify({ address: delegateForm.address, label: delegateForm.label })
434
+ });
435
+ if (!response.ok) {
436
+ throw new Error('Failed to add proposer');
437
+ }
438
+ const payload = (await response.json()) as SafeDelegate[];
439
+ setCurrentSafe((prev) => (prev ? { ...prev, delegates: payload } : prev));
440
+ setDelegateForm({ address: '', label: '' });
441
+ setActionMessage('Proposer added');
442
+ } catch (err) {
443
+ setActionError(err instanceof Error ? err.message : 'Unable to add proposer');
444
+ }
445
+ };
446
+
447
+ const handleRemoveDelegate = async (address: string) => {
448
+ if (!currentSafe) {
449
+ return;
450
+ }
451
+ setActionMessage(undefined);
452
+ setActionError(undefined);
453
+ try {
454
+ const response = await fetch(
455
+ buildBackendUrl(`/api/safes/${currentSafe.address}/delegates/${address}`),
456
+ { method: 'DELETE' }
457
+ );
458
+ if (!response.ok) {
459
+ throw new Error('Failed to remove proposer');
460
+ }
461
+ const payload = (await response.json()) as SafeDelegate[];
462
+ setCurrentSafe((prev) => (prev ? { ...prev, delegates: payload } : prev));
463
+ setActionMessage('Proposer removed');
464
+ } catch (err) {
465
+ setActionError(err instanceof Error ? err.message : 'Unable to remove proposer');
466
+ }
467
+ };
468
+
469
+ const handleFallbackUpdate = async (event: FormEvent<HTMLFormElement>) => {
470
+ event.preventDefault();
471
+ if (!currentSafe) {
472
+ return;
473
+ }
474
+ setActionMessage(undefined);
475
+ setActionError(undefined);
476
+ try {
477
+ const response = await fetch(buildBackendUrl(`/api/safes/${currentSafe.address}/fallback`), {
478
+ method: 'POST',
479
+ headers: { 'Content-Type': 'application/json' },
480
+ body: JSON.stringify({ handler: fallbackForm || undefined })
481
+ });
482
+ if (!response.ok) {
483
+ throw new Error('Failed to update fallback handler');
484
+ }
485
+ const payload = (await response.json()) as { fallbackHandler?: string };
486
+ setCurrentSafe((prev) => (prev ? { ...prev, fallbackHandler: payload.fallbackHandler } : prev));
487
+ setFallbackForm(payload.fallbackHandler ?? '');
488
+ setActionMessage('Fallback handler updated');
489
+ } catch (err) {
490
+ setActionError(err instanceof Error ? err.message : 'Unable to update fallback handler');
491
+ }
492
+ };
493
+
494
+ const handleGuardUpdate = async (event: FormEvent<HTMLFormElement>) => {
495
+ event.preventDefault();
496
+ if (!currentSafe) {
497
+ return;
498
+ }
499
+ setActionMessage(undefined);
500
+ setActionError(undefined);
501
+ try {
502
+ const response = await fetch(buildBackendUrl(`/api/safes/${currentSafe.address}/guard`), {
503
+ method: 'POST',
504
+ headers: { 'Content-Type': 'application/json' },
505
+ body: JSON.stringify({ guard: guardForm || undefined })
506
+ });
507
+ if (!response.ok) {
508
+ throw new Error('Failed to update guard');
509
+ }
510
+ const payload = (await response.json()) as { guard?: string };
511
+ setCurrentSafe((prev) => (prev ? { ...prev, guard: payload.guard } : prev));
512
+ setGuardForm(payload.guard ?? '');
513
+ setActionMessage('Guard updated');
514
+ } catch (err) {
515
+ setActionError(err instanceof Error ? err.message : 'Unable to update guard');
516
+ }
517
+ };
518
+
519
+ const handleProposeTransaction = async (event: FormEvent<HTMLFormElement>) => {
520
+ event.preventDefault();
521
+ if (!currentSafe) {
522
+ return;
523
+ }
524
+ setTxMessage(undefined);
525
+ setTxError(undefined);
526
+ setTxLoading(true);
527
+ try {
528
+ const response = await fetch(buildBackendUrl(`/api/safes/${currentSafe.address}/transactions`), {
529
+ method: 'POST',
530
+ headers: { 'Content-Type': 'application/json' },
531
+ body: JSON.stringify({
532
+ tx: {
533
+ to: txForm.to,
534
+ value: txForm.value || undefined,
535
+ data: txForm.data || undefined
536
+ },
537
+ meta: {
538
+ createdBy: 'gui'
539
+ }
540
+ })
541
+ });
542
+ if (!response.ok) {
543
+ throw new Error('Failed to propose transaction');
544
+ }
545
+ const payload = (await response.json()) as { hash: string };
546
+ setTxMessage(`Transaction proposed: ${payload.hash}`);
547
+ setTxForm({ to: '', value: '', data: '' });
548
+ } catch (err) {
549
+ setTxError(err instanceof Error ? err.message : 'Unable to propose transaction');
550
+ } finally {
551
+ setTxLoading(false);
552
+ }
553
+ };
554
+
555
+ const derivedBalance = useMemo(() => {
556
+ if (!currentSafe?.balance) {
557
+ return 'Not yet synced';
558
+ }
559
+ return currentSafe.balance.includes('ETH') ? currentSafe.balance : `${currentSafe.balance} ETH`;
560
+ }, [currentSafe?.balance]);
561
+
562
+ return (
563
+ <div className="space-y-6">
564
+ <section className="rounded-lg border border-slate-800 bg-slate-900/60 p-4">
565
+ <h2 className="text-lg font-semibold">Connect Safe</h2>
566
+ <form className="mt-4 grid gap-4 md:grid-cols-2" onSubmit={handleConnect}>
567
+ <label className="text-sm text-slate-300">
568
+ Safe Address
569
+ <input
570
+ name="address"
571
+ required
572
+ placeholder="0x..."
573
+ className="mt-1 w-full rounded border border-slate-700 bg-slate-900 p-2"
574
+ />
575
+ </label>
576
+ <div className="rounded border border-slate-800 bg-slate-950/60 p-3 text-xs text-slate-400 md:col-span-2">
577
+ RPC endpoint is resolved automatically from <span className="font-semibold text-slate-200">GNOMAN_RPC_URL</span> or a
578
+ keyring secret named <span className="font-semibold text-slate-200">RPC_URL</span>.
579
+ </div>
580
+ <button
581
+ type="submit"
582
+ disabled={loading}
583
+ className="col-span-full rounded bg-blue-500 px-4 py-2 text-sm font-semibold text-blue-950 transition hover:bg-blue-400 disabled:opacity-50"
584
+ >
585
+ {loading ? 'Connecting...' : 'Connect Safe'}
586
+ </button>
587
+ </form>
588
+ {error && <p className="mt-3 text-sm text-red-400">{error}</p>}
589
+ </section>
590
+
591
+ {currentSafe && (
592
+ <section className="space-y-4 rounded-lg border border-slate-800 bg-slate-900/60 p-4">
593
+ <div className="flex flex-wrap items-center justify-between gap-2">
594
+ <div>
595
+ <h2 className="text-lg font-semibold">Owners</h2>
596
+ <p className="text-xs text-slate-500">
597
+ Threshold {currentSafe.threshold} • Balance {derivedBalance} • Safe {((currentSafe as { safeVersion?: string }).safeVersion ?? 'unknown')} • Mastercopy {((currentSafe as { mastercopyAddress?: string }).mastercopyAddress ?? 'n/a')} • Module enabled {String((currentSafe as { moduleEnabled?: boolean }).moduleEnabled ?? ((currentSafe.modules?.length ?? 0) > 0))}
598
+ </p>
599
+ </div>
600
+ <div className="flex items-center gap-2">
601
+ <button
602
+ onClick={() =>
603
+ refreshSafe(currentSafe.address).catch((err) =>
604
+ setError(err instanceof Error ? err.message : String(err))
605
+ )
606
+ }
607
+ className="rounded border border-slate-700 px-3 py-1 text-xs text-slate-300 transition hover:bg-slate-800"
608
+ >
609
+ Reload
610
+ </button>
611
+ <button
612
+ onClick={syncSafe}
613
+ className="rounded border border-emerald-700/60 px-3 py-1 text-xs font-semibold text-emerald-300 transition hover:bg-emerald-900/40"
614
+ >
615
+ Sync onchain
616
+ </button>
617
+ <button
618
+ onClick={openDetails}
619
+ className="rounded border border-blue-700/70 px-3 py-1 text-xs font-semibold text-blue-300 transition hover:bg-blue-900/40"
620
+ >
621
+ Safe properties
622
+ </button>
623
+ </div>
624
+ </div>
625
+ {(actionMessage || actionError) && (
626
+ <p className={`text-xs ${actionError ? 'text-red-400' : 'text-emerald-400'}`}>
627
+ {actionError ?? actionMessage}
628
+ </p>
629
+ )}
630
+ <ul className="mt-2 space-y-2">
631
+ {currentSafe.owners.map((owner) => (
632
+ <li key={owner} className="rounded border border-slate-800 bg-slate-950/60 p-2 font-mono text-xs">
633
+ {owner}
634
+ </li>
635
+ ))}
636
+ {currentSafe.owners.length === 0 && (
637
+ <li className="rounded border border-dashed border-slate-700 p-3 text-sm text-slate-500">
638
+ Owners will appear here once synchronized.
639
+ </li>
640
+ )}
641
+ </ul>
642
+ <div>
643
+ <h2 className="text-lg font-semibold">Modules</h2>
644
+ <ul className="mt-2 flex flex-wrap gap-2 text-xs">
645
+ {currentSafe.modules.map((module) => (
646
+ <li key={module} className="flex items-center gap-2 rounded border border-slate-700 px-2 py-1 font-mono">
647
+ {module}
648
+ <button
649
+ onClick={() => handleRemoveModule(module)}
650
+ className="rounded border border-slate-600 px-1 text-[10px] text-slate-300 transition hover:bg-slate-800"
651
+ >
652
+ Remove
653
+ </button>
654
+ </li>
655
+ ))}
656
+ {currentSafe.modules.length === 0 && <p className="text-sm text-slate-500">No modules enabled.</p>}
657
+ </ul>
658
+ </div>
659
+ <div className="rounded border border-slate-800 bg-slate-950/60 p-4 text-sm text-slate-300">
660
+ <h3 className="text-base font-semibold text-slate-200">Propose transaction</h3>
661
+ <form className="mt-3 space-y-3" onSubmit={handleProposeTransaction}>
662
+ <label className="text-xs uppercase tracking-widest text-slate-500">To address</label>
663
+ <input
664
+ value={txForm.to}
665
+ onChange={(event) => setTxForm((prev) => ({ ...prev, to: event.target.value }))}
666
+ placeholder="0x..."
667
+ className="w-full rounded border border-slate-700 bg-slate-900 p-2 text-xs"
668
+ required
669
+ />
670
+ <label className="text-xs uppercase tracking-widest text-slate-500">Value (ETH)</label>
671
+ <input
672
+ value={txForm.value}
673
+ onChange={(event) => setTxForm((prev) => ({ ...prev, value: event.target.value }))}
674
+ placeholder="0.0"
675
+ className="w-full rounded border border-slate-700 bg-slate-900 p-2 text-xs"
676
+ />
677
+ <label className="text-xs uppercase tracking-widest text-slate-500">Data (optional)</label>
678
+ <textarea
679
+ value={txForm.data}
680
+ onChange={(event) => setTxForm((prev) => ({ ...prev, data: event.target.value }))}
681
+ placeholder="0x"
682
+ rows={3}
683
+ className="w-full rounded border border-slate-700 bg-slate-900 p-2 text-xs"
684
+ />
685
+ <button
686
+ type="submit"
687
+ disabled={txLoading}
688
+ className="w-full rounded bg-blue-500 px-4 py-2 text-xs font-semibold text-blue-950 transition hover:bg-blue-400 disabled:opacity-50"
689
+ >
690
+ {txLoading ? 'Submitting...' : 'Propose transaction'}
691
+ </button>
692
+ {(txMessage || txError) && (
693
+ <p className={`text-xs ${txError ? 'text-red-400' : 'text-emerald-400'}`}>
694
+ {txError ?? txMessage}
695
+ </p>
696
+ )}
697
+ </form>
698
+ </div>
699
+ <div className="grid gap-4 lg:grid-cols-2">
700
+ <div className="rounded border border-slate-800 bg-slate-950/60 p-4 text-sm text-slate-300">
701
+ <h3 className="text-base font-semibold text-slate-200">Owner & threshold controls</h3>
702
+ <form className="mt-3 space-y-3" onSubmit={handleAddOwner}>
703
+ <label className="text-xs uppercase tracking-widest text-slate-500">Add owner</label>
704
+ <input
705
+ value={ownerForm.address}
706
+ onChange={(event) => setOwnerForm((prev) => ({ ...prev, address: event.target.value }))}
707
+ placeholder="0x..."
708
+ className="w-full rounded border border-slate-700 bg-slate-900 p-2 text-xs"
709
+ />
710
+ <label className="flex flex-col gap-1 text-xs text-slate-400">
711
+ Threshold
712
+ <input
713
+ type="number"
714
+ min={1}
715
+ value={ownerForm.threshold}
716
+ onChange={(event) =>
717
+ setOwnerForm((prev) => ({ ...prev, threshold: Number(event.target.value) }))
718
+ }
719
+ className="rounded border border-slate-700 bg-slate-900 p-2 text-xs text-slate-100"
720
+ />
721
+ </label>
722
+ <button
723
+ type="submit"
724
+ className="w-full rounded bg-emerald-500/90 px-3 py-2 text-xs font-semibold text-emerald-950 transition hover:bg-emerald-400"
725
+ >
726
+ Add owner
727
+ </button>
728
+ </form>
729
+ <form className="mt-4 space-y-3" onSubmit={handleRemoveOwner}>
730
+ <label className="text-xs uppercase tracking-widest text-slate-500">Remove owner</label>
731
+ <input
732
+ value={ownerRemoveForm.address}
733
+ onChange={(event) => setOwnerRemoveForm((prev) => ({ ...prev, address: event.target.value }))}
734
+ placeholder="0x..."
735
+ className="w-full rounded border border-slate-700 bg-slate-900 p-2 text-xs"
736
+ />
737
+ <label className="flex flex-col gap-1 text-xs text-slate-400">
738
+ Threshold after removal
739
+ <input
740
+ type="number"
741
+ min={1}
742
+ value={ownerRemoveForm.threshold}
743
+ onChange={(event) =>
744
+ setOwnerRemoveForm((prev) => ({ ...prev, threshold: Number(event.target.value) }))
745
+ }
746
+ className="rounded border border-slate-700 bg-slate-900 p-2 text-xs text-slate-100"
747
+ />
748
+ </label>
749
+ <button
750
+ type="submit"
751
+ className="w-full rounded bg-amber-500/90 px-3 py-2 text-xs font-semibold text-amber-950 transition hover:bg-amber-400"
752
+ >
753
+ Remove owner
754
+ </button>
755
+ </form>
756
+ <form className="mt-4 space-y-3" onSubmit={handleThresholdUpdate}>
757
+ <label className="text-xs uppercase tracking-widest text-slate-500">Set threshold</label>
758
+ <input
759
+ type="number"
760
+ min={1}
761
+ value={thresholdForm}
762
+ onChange={(event) => setThresholdForm(Number(event.target.value))}
763
+ className="w-full rounded border border-slate-700 bg-slate-900 p-2 text-xs text-slate-100"
764
+ />
765
+ <button
766
+ type="submit"
767
+ className="w-full rounded bg-blue-500/90 px-3 py-2 text-xs font-semibold text-blue-950 transition hover:bg-blue-400"
768
+ >
769
+ Update threshold
770
+ </button>
771
+ </form>
772
+ </div>
773
+ <div className="space-y-4">
774
+ <div className="rounded border border-slate-800 bg-slate-950/60 p-4 text-sm text-slate-300">
775
+ <h3 className="text-base font-semibold text-slate-200">Module controls</h3>
776
+ <form className="mt-3 space-y-3" onSubmit={handleAddModule}>
777
+ <label className="text-xs uppercase tracking-widest text-slate-500">Enable module</label>
778
+ <input
779
+ value={moduleForm}
780
+ onChange={(event) => setModuleForm(event.target.value)}
781
+ placeholder="0x..."
782
+ className="w-full rounded border border-slate-700 bg-slate-900 p-2 text-xs"
783
+ />
784
+ <button
785
+ type="submit"
786
+ className="w-full rounded bg-emerald-500/90 px-3 py-2 text-xs font-semibold text-emerald-950 transition hover:bg-emerald-400"
787
+ >
788
+ Enable module
789
+ </button>
790
+ </form>
791
+ </div>
792
+ <div className="rounded border border-slate-800 bg-slate-950/60 p-4 text-sm text-slate-300">
793
+ <h3 className="text-base font-semibold text-slate-200">Proposers</h3>
794
+ <form className="mt-3 space-y-3" onSubmit={handleAddDelegate}>
795
+ <label className="text-xs uppercase tracking-widest text-slate-500">Add proposer</label>
796
+ <input
797
+ value={delegateForm.address}
798
+ onChange={(event) => setDelegateForm((prev) => ({ ...prev, address: event.target.value }))}
799
+ placeholder="0x..."
800
+ className="w-full rounded border border-slate-700 bg-slate-900 p-2 text-xs"
801
+ />
802
+ <input
803
+ value={delegateForm.label}
804
+ onChange={(event) => setDelegateForm((prev) => ({ ...prev, label: event.target.value }))}
805
+ placeholder="Label"
806
+ className="w-full rounded border border-slate-700 bg-slate-900 p-2 text-xs"
807
+ />
808
+ <button
809
+ type="submit"
810
+ className="w-full rounded bg-purple-500/90 px-3 py-2 text-xs font-semibold text-purple-950 transition hover:bg-purple-400"
811
+ >
812
+ Add proposer
813
+ </button>
814
+ </form>
815
+ <ul className="mt-3 space-y-2 text-xs">
816
+ {(currentSafe.delegates ?? []).map((delegate) => (
817
+ <li
818
+ key={delegate.address}
819
+ className="flex flex-wrap items-center justify-between gap-2 rounded border border-slate-800 bg-slate-950/80 px-3 py-2"
820
+ >
821
+ <div>
822
+ <p className="font-semibold text-slate-200">{delegate.label}</p>
823
+ <p className="font-mono text-[10px] text-slate-400">{delegate.address}</p>
824
+ </div>
825
+ <button
826
+ onClick={() => handleRemoveDelegate(delegate.address)}
827
+ className="rounded border border-slate-700 px-2 py-1 text-[11px] text-slate-300 transition hover:bg-slate-800"
828
+ >
829
+ Remove
830
+ </button>
831
+ </li>
832
+ ))}
833
+ {(currentSafe.delegates ?? []).length === 0 && (
834
+ <li className="text-xs text-slate-500">No proposers registered.</li>
835
+ )}
836
+ </ul>
837
+ </div>
838
+ <div className="rounded border border-slate-800 bg-slate-950/60 p-4 text-sm text-slate-300">
839
+ <h3 className="text-base font-semibold text-slate-200">Fallback & guard</h3>
840
+ <form className="mt-3 space-y-3" onSubmit={handleFallbackUpdate}>
841
+ <label className="text-xs uppercase tracking-widest text-slate-500">Fallback handler</label>
842
+ <input
843
+ value={fallbackForm}
844
+ onChange={(event) => setFallbackForm(event.target.value)}
845
+ placeholder="0x... (empty to clear)"
846
+ className="w-full rounded border border-slate-700 bg-slate-900 p-2 text-xs"
847
+ />
848
+ <button
849
+ type="submit"
850
+ className="w-full rounded bg-cyan-500/90 px-3 py-2 text-xs font-semibold text-cyan-950 transition hover:bg-cyan-400"
851
+ >
852
+ Update fallback
853
+ </button>
854
+ </form>
855
+ <form className="mt-4 space-y-3" onSubmit={handleGuardUpdate}>
856
+ <label className="text-xs uppercase tracking-widest text-slate-500">Guard</label>
857
+ <input
858
+ value={guardForm}
859
+ onChange={(event) => setGuardForm(event.target.value)}
860
+ placeholder="0x... (empty to clear)"
861
+ className="w-full rounded border border-slate-700 bg-slate-900 p-2 text-xs"
862
+ />
863
+ <button
864
+ type="submit"
865
+ className="w-full rounded bg-indigo-500/90 px-3 py-2 text-xs font-semibold text-indigo-950 transition hover:bg-indigo-400"
866
+ >
867
+ Update guard
868
+ </button>
869
+ </form>
870
+ </div>
871
+ </div>
872
+ </div>
873
+ <div className="rounded border border-slate-800 bg-slate-950/60 p-3 text-sm text-slate-300">
874
+ <form className="space-y-3" onSubmit={handleHoldSubmit}>
875
+ <div className="flex items-center justify-between gap-3">
876
+ <div>
877
+ <h3 className="text-base font-semibold text-slate-200">Safe hold policy</h3>
878
+ <p className="text-xs text-slate-500">
879
+ Global default: {holdPolicy?.global.enabled ? 'Enabled' : 'Disabled'} ·{' '}
880
+ {holdPolicy?.global.holdHours ?? 24}h
881
+ </p>
882
+ </div>
883
+ <label className="inline-flex items-center gap-2 text-xs font-medium">
884
+ <input
885
+ type="checkbox"
886
+ className="h-4 w-4 rounded border-slate-700 bg-slate-900 text-blue-500 focus:ring-blue-500"
887
+ checked={holdForm.enabled}
888
+ onChange={(event) =>
889
+ setHoldForm((prev) => ({ ...prev, enabled: event.target.checked }))
890
+ }
891
+ />
892
+ Enable
893
+ </label>
894
+ </div>
895
+ <label className="flex flex-col gap-1 text-xs text-slate-300">
896
+ Hold duration (hours)
897
+ <input
898
+ type="number"
899
+ min={1}
900
+ max={24 * 14}
901
+ className="rounded border border-slate-800 bg-slate-900 p-2 text-sm text-white focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
902
+ value={holdForm.holdHours}
903
+ onChange={(event) => {
904
+ const value = Number.parseInt(event.target.value, 10);
905
+ setHoldForm((prev) => ({
906
+ ...prev,
907
+ holdHours: Number.isNaN(value)
908
+ ? prev.holdHours
909
+ : Math.max(1, Math.min(value, 24 * 14))
910
+ }));
911
+ }}
912
+ disabled={!holdForm.enabled}
913
+ />
914
+ </label>
915
+ <div className="flex items-center justify-between text-xs text-slate-400">
916
+ <span>Pending holds: {holdSummary.pending}</span>
917
+ <span>Executed via hold: {holdSummary.executed}</span>
918
+ </div>
919
+ {holdMessage && (
920
+ <p className={`text-xs ${holdMessage.includes('saved') ? 'text-emerald-400' : 'text-red-400'}`}>
921
+ {holdMessage}
922
+ </p>
923
+ )}
924
+ <button
925
+ type="submit"
926
+ className="w-full rounded bg-blue-600 px-4 py-2 text-xs font-semibold text-white transition hover:bg-blue-500 disabled:cursor-not-allowed disabled:bg-blue-900"
927
+ disabled={holdSaving}
928
+ >
929
+ {holdSaving ? 'Saving…' : 'Save policy'}
930
+ </button>
931
+ </form>
932
+ </div>
933
+ <div>
934
+ <h2 className="text-lg font-semibold">Held Transactions</h2>
935
+ <ul className="mt-2 space-y-2 text-xs">
936
+ {heldTransactions.map((tx) => (
937
+ <li key={tx.txHash} className="space-y-2 rounded border border-slate-800 bg-slate-950/60 p-3">
938
+ <div className="flex flex-col gap-1 text-[11px] text-slate-300 md:flex-row md:items-center md:justify-between">
939
+ <span className="font-mono text-[10px] text-slate-400">{tx.txHash}</span>
940
+ <span className="font-medium text-slate-200">Countdown: {countdowns[tx.txHash] ?? '…'}</span>
941
+ </div>
942
+ <div className="flex flex-wrap gap-2 text-[11px] text-slate-400">
943
+ <span>Hold until {new Date(tx.holdUntil).toLocaleString()}</span>
944
+ <span>Duration {tx.holdHours}h</span>
945
+ <span>Status {tx.executed ? 'Executed' : 'Pending'}</span>
946
+ </div>
947
+ <button
948
+ onClick={() => releaseHold(tx.txHash)}
949
+ className="rounded bg-amber-500/90 px-3 py-1 text-[11px] font-semibold text-amber-950 transition hover:bg-amber-400"
950
+ >
951
+ Release now
952
+ </button>
953
+ </li>
954
+ ))}
955
+ {heldTransactions.length === 0 && <p className="text-sm text-slate-500">No held transactions.</p>}
956
+ </ul>
957
+ </div>
958
+ </section>
959
+ )}
960
+
961
+ {detailsOpen && (
962
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80 p-4">
963
+ <div className="relative w-full max-w-3xl rounded-xl border border-slate-800 bg-slate-900 p-6 text-sm text-slate-200 shadow-2xl">
964
+ <button
965
+ onClick={closeDetails}
966
+ className="absolute right-4 top-4 rounded-full border border-slate-700 px-2 py-0.5 text-xs text-slate-300 transition hover:bg-slate-800"
967
+ >
968
+ Close
969
+ </button>
970
+ <h3 className="text-xl font-semibold text-white">Safe properties</h3>
971
+ <p className="mt-1 font-mono text-xs text-slate-400">{currentSafe?.address}</p>
972
+ <div className="mt-4 space-y-3">
973
+ {detailsLoading && <p className="text-slate-400">Loading Safe telemetry…</p>}
974
+ {detailsError && <p className="text-red-400">{detailsError}</p>}
975
+ {!detailsLoading && !detailsError && details && (
976
+ <div className="space-y-4">
977
+ <div className="grid gap-4 sm:grid-cols-2">
978
+ <div>
979
+ <p className="text-xs uppercase tracking-widest text-slate-500">Network</p>
980
+ <p className="text-base font-semibold text-white">{details.network ?? 'Unknown'}</p>
981
+ </div>
982
+ <div>
983
+ <p className="text-xs uppercase tracking-widest text-slate-500">Balance</p>
984
+ <p className="text-base font-semibold text-white">
985
+ {details.balance
986
+ ? details.balance.includes('ETH')
987
+ ? details.balance
988
+ : `${details.balance} ETH`
989
+ : 'Not yet synced'}
990
+ </p>
991
+ </div>
992
+ <div>
993
+ <p className="text-xs uppercase tracking-widest text-slate-500">Threshold</p>
994
+ <p className="text-base font-semibold text-white">
995
+ {details.threshold} of {details.owners.length} owners
996
+ </p>
997
+ </div>
998
+ <div>
999
+ <p className="text-xs uppercase tracking-widest text-slate-500">RPC endpoint</p>
1000
+ <p className="break-all text-xs text-slate-300">{details.rpcUrl}</p>
1001
+ </div>
1002
+ <div>
1003
+ <p className="text-xs uppercase tracking-widest text-slate-500">Hold policy</p>
1004
+ <p className="text-xs text-slate-300">
1005
+ {details.holdPolicy.enabled ? 'Enabled' : 'Disabled'} · {details.holdPolicy.holdHours}h lock · Updated{' '}
1006
+ {formatPolicyUpdatedAt(details.holdPolicy.updatedAt)}
1007
+ </p>
1008
+ </div>
1009
+ <div>
1010
+ <p className="text-xs uppercase tracking-widest text-slate-500">Fallback handler</p>
1011
+ <p className="break-all text-xs text-slate-300">{details.fallbackHandler ?? 'Not configured'}</p>
1012
+ </div>
1013
+ <div>
1014
+ <p className="text-xs uppercase tracking-widest text-slate-500">Guard</p>
1015
+ <p className="break-all text-xs text-slate-300">{details.guard ?? 'Not configured'}</p>
1016
+ </div>
1017
+ </div>
1018
+ <div className="rounded-lg border border-slate-800 bg-slate-950/60 p-4">
1019
+ <h4 className="text-sm font-semibold text-white">Delegates</h4>
1020
+ <ul className="mt-3 space-y-2 text-xs">
1021
+ {details.delegates.length === 0 && (
1022
+ <li className="text-slate-500">No delegates registered.</li>
1023
+ )}
1024
+ {details.delegates.map((delegate) => (
1025
+ <li
1026
+ key={`${delegate.address}-${delegate.label}`}
1027
+ className="flex flex-col gap-1 rounded border border-slate-800 bg-slate-950/80 p-3 sm:flex-row sm:items-center sm:justify-between"
1028
+ >
1029
+ <span className="font-semibold text-slate-200">{delegate.label}</span>
1030
+ <span className="font-mono text-[11px] text-emerald-300">{delegate.address}</span>
1031
+ <span className="text-[11px] text-slate-400">
1032
+ Since {new Date(delegate.since).toLocaleString()}
1033
+ </span>
1034
+ </li>
1035
+ ))}
1036
+ </ul>
1037
+ </div>
1038
+ <div className="rounded-lg border border-slate-800 bg-slate-950/60 p-4">
1039
+ <h4 className="text-sm font-semibold text-white">Owners ({details.owners.length})</h4>
1040
+ <ul className="mt-3 space-y-2 text-xs">
1041
+ {details.owners.map((owner) => (
1042
+ <li
1043
+ key={owner}
1044
+ className="rounded border border-slate-800 bg-slate-950/80 p-2 font-mono text-[11px] text-slate-300"
1045
+ >
1046
+ {owner}
1047
+ </li>
1048
+ ))}
1049
+ {details.owners.length === 0 && <li className="text-slate-500">Owners will populate after synchronization.</li>}
1050
+ </ul>
1051
+ </div>
1052
+ <div className="rounded-lg border border-slate-800 bg-slate-950/60 p-4">
1053
+ <h4 className="text-sm font-semibold text-white">Modules ({details.modules.length})</h4>
1054
+ <ul className="mt-3 flex flex-wrap gap-2 text-[11px]">
1055
+ {details.modules.map((module) => (
1056
+ <li key={module} className="rounded border border-slate-800 bg-slate-950/80 px-2 py-1 font-mono text-emerald-300">
1057
+ {module}
1058
+ </li>
1059
+ ))}
1060
+ {details.modules.length === 0 && <li className="text-slate-500">No automation modules linked.</li>}
1061
+ </ul>
1062
+ </div>
1063
+ <div className="grid gap-3 sm:grid-cols-2">
1064
+ <div className="rounded-lg border border-slate-800 bg-slate-950/60 p-4">
1065
+ <p className="text-xs uppercase tracking-widest text-slate-500">Held transactions</p>
1066
+ <p className="mt-2 text-lg font-semibold text-white">Pending {details.holdSummary.pending}</p>
1067
+ <p className="text-xs text-slate-400">Executed in hold window: {details.holdSummary.executed}</p>
1068
+ </div>
1069
+ <div className="rounded-lg border border-slate-800 bg-slate-950/60 p-4">
1070
+ <p className="text-xs uppercase tracking-widest text-slate-500">Effective policy</p>
1071
+ <p className="mt-2 text-xs text-slate-300">
1072
+ Global: {details.effectiveHold.global.enabled ? 'On' : 'Off'} · {details.effectiveHold.global.holdHours}h
1073
+ </p>
1074
+ <p className="text-xs text-slate-300">
1075
+ Local: {details.effectiveHold.local.enabled ? 'On' : 'Off'} · {details.effectiveHold.local.holdHours}h
1076
+ </p>
1077
+ </div>
1078
+ </div>
1079
+ </div>
1080
+ )}
1081
+ </div>
1082
+ </div>
1083
+ </div>
1084
+ )}
1085
+ </div>
1086
+ );
1087
+ };
1088
+
1089
+ export default Safes;