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.
- package/.eslintrc.cjs +24 -0
- package/.gnoman/contracts.json +4094 -0
- package/.gnoman/exec_package/runtime-debug.jsonl +45 -0
- package/.gnoman/holds.sqlite +0 -0
- package/.gnoman/license.json +7 -0
- package/.gnoman/safes.json +37 -0
- package/.gnoman/vanity-jobs.json +67 -0
- package/.gnoman/wallets.db +0 -0
- package/.prettierrc.json +6 -0
- package/CODex_TASKS.md +124 -0
- package/LICENSE.md +164 -0
- package/README.md +95 -0
- package/assets/GnoLogo.jpg +0 -0
- package/assets/self.png +0 -0
- package/backend/controllers/contractController.ts +49 -0
- package/backend/controllers/devToolsController.ts +76 -0
- package/backend/controllers/etherscanController.ts +59 -0
- package/backend/controllers/historyController.ts +7 -0
- package/backend/controllers/keyringController.ts +134 -0
- package/backend/controllers/robinhoodController.ts +80 -0
- package/backend/controllers/safeController.ts +167 -0
- package/backend/controllers/sandboxController.ts +63 -0
- package/backend/controllers/settingsController.ts +38 -0
- package/backend/controllers/walletController.ts +151 -0
- package/backend/index.ts +133 -0
- package/backend/licenses/license_public.pem +3 -0
- package/backend/licenses/verify_license.py +43 -0
- package/backend/routes/contractRoutes.ts +11 -0
- package/backend/routes/devToolsRoutes.ts +11 -0
- package/backend/routes/etherscanRoutes.ts +11 -0
- package/backend/routes/historyRoutes.ts +8 -0
- package/backend/routes/keyringRoutes.ts +25 -0
- package/backend/routes/licenseRoutes.ts +35 -0
- package/backend/routes/robinhoodRoutes.ts +22 -0
- package/backend/routes/runtimeRoutes.ts +29 -0
- package/backend/routes/safeRoutes.ts +28 -0
- package/backend/routes/sandboxRoutes.ts +17 -0
- package/backend/routes/settingsRoutes.ts +14 -0
- package/backend/routes/walletRoutes.ts +21 -0
- package/backend/services/chainlinkService.ts +65 -0
- package/backend/services/contractRegistryService.ts +205 -0
- package/backend/services/devToolsService.ts +251 -0
- package/backend/services/diagnosticsService.ts +350 -0
- package/backend/services/etherscanService.ts +152 -0
- package/backend/services/historyService.ts +89 -0
- package/backend/services/keyringAccessor.ts +4 -0
- package/backend/services/licenseService.ts +163 -0
- package/backend/services/onchain/abiRegistry.ts +57 -0
- package/backend/services/onchain/chainlinkClient.ts +56 -0
- package/backend/services/onchain/errors.ts +16 -0
- package/backend/services/onchain/etherscanClient.ts +94 -0
- package/backend/services/onchain/index.ts +76 -0
- package/backend/services/onchain/tenderlyRpcClient.ts +74 -0
- package/backend/services/onchain/types.ts +33 -0
- package/backend/services/onchainAutomationService.ts +424 -0
- package/backend/services/robinhood/auth.ts +42 -0
- package/backend/services/robinhood/client.ts +123 -0
- package/backend/services/robinhood/integrationService.ts +140 -0
- package/backend/services/robinhood/provider.ts +22 -0
- package/backend/services/robinhood/unofficialClient.ts +66 -0
- package/backend/services/rpcService.ts +44 -0
- package/backend/services/runtimeTelemetryService.ts +158 -0
- package/backend/services/safeConfigRepository.ts +205 -0
- package/backend/services/safeService.ts +588 -0
- package/backend/services/sandboxService.ts +157 -0
- package/backend/services/secureSettingsService.ts +45 -0
- package/backend/services/transactionHoldService.ts +223 -0
- package/backend/services/vanityService.ts +293 -0
- package/backend/services/walletService.ts +290 -0
- package/backend/services/walletStore.ts +179 -0
- package/backend/types/express-async-handler.d.ts +13 -0
- package/backend/types/keyring.d.ts +19 -0
- package/backend/utils/abiResolver.ts +208 -0
- package/backend/utils/http.ts +6 -0
- package/backend/utils/secretsResolver.ts +150 -0
- package/backend/utils/signer.ts +11 -0
- package/backend/workers/vanityWorker.ts +76 -0
- package/capacitor.config.ts +13 -0
- package/cli/gnoman.ts +424 -0
- package/contracts/OracleConsumer.sol +20 -0
- package/contracts/PriceFeedConsumer.sol +22 -0
- package/dist/backend/backend/controllers/contractController.js +41 -0
- package/dist/backend/backend/controllers/contractController.js.map +1 -0
- package/dist/backend/backend/controllers/devToolsController.js +63 -0
- package/dist/backend/backend/controllers/devToolsController.js.map +1 -0
- package/dist/backend/backend/controllers/etherscanController.js +53 -0
- package/dist/backend/backend/controllers/etherscanController.js.map +1 -0
- package/dist/backend/backend/controllers/historyController.js +12 -0
- package/dist/backend/backend/controllers/historyController.js.map +1 -0
- package/dist/backend/backend/controllers/keyringController.js +126 -0
- package/dist/backend/backend/controllers/keyringController.js.map +1 -0
- package/dist/backend/backend/controllers/robinhoodController.js +69 -0
- package/dist/backend/backend/controllers/robinhoodController.js.map +1 -0
- package/dist/backend/backend/controllers/safeController.js +137 -0
- package/dist/backend/backend/controllers/safeController.js.map +1 -0
- package/dist/backend/backend/controllers/sandboxController.js +48 -0
- package/dist/backend/backend/controllers/sandboxController.js.map +1 -0
- package/dist/backend/backend/controllers/settingsController.js +34 -0
- package/dist/backend/backend/controllers/settingsController.js.map +1 -0
- package/dist/backend/backend/controllers/walletController.js +140 -0
- package/dist/backend/backend/controllers/walletController.js.map +1 -0
- package/dist/backend/backend/index.js +119 -0
- package/dist/backend/backend/index.js.map +1 -0
- package/dist/backend/backend/routes/contractRoutes.js +44 -0
- package/dist/backend/backend/routes/contractRoutes.js.map +1 -0
- package/dist/backend/backend/routes/devToolsRoutes.js +44 -0
- package/dist/backend/backend/routes/devToolsRoutes.js.map +1 -0
- package/dist/backend/backend/routes/etherscanRoutes.js +44 -0
- package/dist/backend/backend/routes/etherscanRoutes.js.map +1 -0
- package/dist/backend/backend/routes/historyRoutes.js +41 -0
- package/dist/backend/backend/routes/historyRoutes.js.map +1 -0
- package/dist/backend/backend/routes/keyringRoutes.js +18 -0
- package/dist/backend/backend/routes/keyringRoutes.js.map +1 -0
- package/dist/backend/backend/routes/licenseRoutes.js +30 -0
- package/dist/backend/backend/routes/licenseRoutes.js.map +1 -0
- package/dist/backend/backend/routes/robinhoodRoutes.js +14 -0
- package/dist/backend/backend/routes/robinhoodRoutes.js.map +1 -0
- package/dist/backend/backend/routes/runtimeRoutes.js +26 -0
- package/dist/backend/backend/routes/runtimeRoutes.js.map +1 -0
- package/dist/backend/backend/routes/safeRoutes.js +61 -0
- package/dist/backend/backend/routes/safeRoutes.js.map +1 -0
- package/dist/backend/backend/routes/sandboxRoutes.js +50 -0
- package/dist/backend/backend/routes/sandboxRoutes.js.map +1 -0
- package/dist/backend/backend/routes/settingsRoutes.js +10 -0
- package/dist/backend/backend/routes/settingsRoutes.js.map +1 -0
- package/dist/backend/backend/routes/walletRoutes.js +54 -0
- package/dist/backend/backend/routes/walletRoutes.js.map +1 -0
- package/dist/backend/backend/services/chainlinkService.js +48 -0
- package/dist/backend/backend/services/chainlinkService.js.map +1 -0
- package/dist/backend/backend/services/contractRegistryService.js +138 -0
- package/dist/backend/backend/services/contractRegistryService.js.map +1 -0
- package/dist/backend/backend/services/devToolsService.js +213 -0
- package/dist/backend/backend/services/devToolsService.js.map +1 -0
- package/dist/backend/backend/services/diagnosticsService.js +286 -0
- package/dist/backend/backend/services/diagnosticsService.js.map +1 -0
- package/dist/backend/backend/services/etherscanService.js +125 -0
- package/dist/backend/backend/services/etherscanService.js.map +1 -0
- package/dist/backend/backend/services/historyService.js +75 -0
- package/dist/backend/backend/services/historyService.js.map +1 -0
- package/dist/backend/backend/services/keyringAccessor.js +40 -0
- package/dist/backend/backend/services/keyringAccessor.js.map +1 -0
- package/dist/backend/backend/services/licenseService.js +130 -0
- package/dist/backend/backend/services/licenseService.js.map +1 -0
- package/dist/backend/backend/services/onchain/abiRegistry.js +47 -0
- package/dist/backend/backend/services/onchain/abiRegistry.js.map +1 -0
- package/dist/backend/backend/services/onchain/chainlinkClient.js +43 -0
- package/dist/backend/backend/services/onchain/chainlinkClient.js.map +1 -0
- package/dist/backend/backend/services/onchain/errors.js +13 -0
- package/dist/backend/backend/services/onchain/errors.js.map +1 -0
- package/dist/backend/backend/services/onchain/etherscanClient.js +82 -0
- package/dist/backend/backend/services/onchain/etherscanClient.js.map +1 -0
- package/dist/backend/backend/services/onchain/index.js +79 -0
- package/dist/backend/backend/services/onchain/index.js.map +1 -0
- package/dist/backend/backend/services/onchain/tenderlyRpcClient.js +60 -0
- package/dist/backend/backend/services/onchain/tenderlyRpcClient.js.map +1 -0
- package/dist/backend/backend/services/onchain/types.js +14 -0
- package/dist/backend/backend/services/onchain/types.js.map +1 -0
- package/dist/backend/backend/services/onchainAutomationService.js +316 -0
- package/dist/backend/backend/services/onchainAutomationService.js.map +1 -0
- package/dist/backend/backend/services/robinhood/auth.js +26 -0
- package/dist/backend/backend/services/robinhood/auth.js.map +1 -0
- package/dist/backend/backend/services/robinhood/client.js +73 -0
- package/dist/backend/backend/services/robinhood/client.js.map +1 -0
- package/dist/backend/backend/services/robinhood/integrationService.js +119 -0
- package/dist/backend/backend/services/robinhood/integrationService.js.map +1 -0
- package/dist/backend/backend/services/robinhood/provider.js +17 -0
- package/dist/backend/backend/services/robinhood/provider.js.map +1 -0
- package/dist/backend/backend/services/robinhood/unofficialClient.js +61 -0
- package/dist/backend/backend/services/robinhood/unofficialClient.js.map +1 -0
- package/dist/backend/backend/services/rpcService.js +48 -0
- package/dist/backend/backend/services/rpcService.js.map +1 -0
- package/dist/backend/backend/services/runtimeTelemetryService.js +96 -0
- package/dist/backend/backend/services/runtimeTelemetryService.js.map +1 -0
- package/dist/backend/backend/services/safeConfigRepository.js +147 -0
- package/dist/backend/backend/services/safeConfigRepository.js.map +1 -0
- package/dist/backend/backend/services/safeService.js +527 -0
- package/dist/backend/backend/services/safeService.js.map +1 -0
- package/dist/backend/backend/services/sandboxService.js +135 -0
- package/dist/backend/backend/services/sandboxService.js.map +1 -0
- package/dist/backend/backend/services/secureSettingsService.js +50 -0
- package/dist/backend/backend/services/secureSettingsService.js.map +1 -0
- package/dist/backend/backend/services/transactionHoldService.js +184 -0
- package/dist/backend/backend/services/transactionHoldService.js.map +1 -0
- package/dist/backend/backend/services/vanityService.js +235 -0
- package/dist/backend/backend/services/vanityService.js.map +1 -0
- package/dist/backend/backend/services/walletService.js +202 -0
- package/dist/backend/backend/services/walletService.js.map +1 -0
- package/dist/backend/backend/services/walletStore.js +132 -0
- package/dist/backend/backend/services/walletStore.js.map +1 -0
- package/dist/backend/backend/utils/abiResolver.js +182 -0
- package/dist/backend/backend/utils/abiResolver.js.map +1 -0
- package/dist/backend/backend/utils/http.js +12 -0
- package/dist/backend/backend/utils/http.js.map +1 -0
- package/dist/backend/backend/utils/secretsResolver.js +137 -0
- package/dist/backend/backend/utils/secretsResolver.js.map +1 -0
- package/dist/backend/backend/utils/signer.js +15 -0
- package/dist/backend/backend/utils/signer.js.map +1 -0
- package/dist/backend/backend/workers/vanityWorker.js +63 -0
- package/dist/backend/backend/workers/vanityWorker.js.map +1 -0
- package/dist/backend/cli/gnoman.js +387 -0
- package/dist/backend/cli/gnoman.js.map +1 -0
- package/dist/backend/modules/sandbox/abiLoader.js +78 -0
- package/dist/backend/modules/sandbox/abiLoader.js.map +1 -0
- package/dist/backend/modules/sandbox/contractSimulator.js +205 -0
- package/dist/backend/modules/sandbox/contractSimulator.js.map +1 -0
- package/dist/backend/modules/sandbox/formBuilder.js +14 -0
- package/dist/backend/modules/sandbox/formBuilder.js.map +1 -0
- package/dist/backend/modules/sandbox/index.js +24 -0
- package/dist/backend/modules/sandbox/index.js.map +1 -0
- package/dist/backend/modules/sandbox/localFork.js +103 -0
- package/dist/backend/modules/sandbox/localFork.js.map +1 -0
- package/dist/backend/modules/sandbox/sandboxManager.js +130 -0
- package/dist/backend/modules/sandbox/sandboxManager.js.map +1 -0
- package/dist/backend/modules/sandbox/types.js +3 -0
- package/dist/backend/modules/sandbox/types.js.map +1 -0
- package/dist/backend/src/core/backends/fileBackend.js +136 -0
- package/dist/backend/src/core/backends/fileBackend.js.map +1 -0
- package/dist/backend/src/core/backends/memoryBackend.js +26 -0
- package/dist/backend/src/core/backends/memoryBackend.js.map +1 -0
- package/dist/backend/src/core/backends/systemBackend.js +86 -0
- package/dist/backend/src/core/backends/systemBackend.js.map +1 -0
- package/dist/backend/src/core/backends/types.js +12 -0
- package/dist/backend/src/core/backends/types.js.map +1 -0
- package/dist/backend/src/core/keyringManager.js +178 -0
- package/dist/backend/src/core/keyringManager.js.map +1 -0
- package/dist/backend/src/utils/abiResolver.js +180 -0
- package/dist/backend/src/utils/abiResolver.js.map +1 -0
- package/dist/backend/src/utils/runtimeObservability.js +78 -0
- package/dist/backend/src/utils/runtimeObservability.js.map +1 -0
- package/dist/backend/src/utils/secretsResolver.js +138 -0
- package/dist/backend/src/utils/secretsResolver.js.map +1 -0
- package/dist/cli/backend/services/diagnosticsService.js +286 -0
- package/dist/cli/backend/services/diagnosticsService.js.map +1 -0
- package/dist/cli/backend/services/keyringAccessor.js +40 -0
- package/dist/cli/backend/services/keyringAccessor.js.map +1 -0
- package/dist/cli/backend/services/rpcService.js +48 -0
- package/dist/cli/backend/services/rpcService.js.map +1 -0
- package/dist/cli/backend/services/runtimeTelemetryService.js +96 -0
- package/dist/cli/backend/services/runtimeTelemetryService.js.map +1 -0
- package/dist/cli/backend/services/walletService.js +202 -0
- package/dist/cli/backend/services/walletService.js.map +1 -0
- package/dist/cli/backend/services/walletStore.js +132 -0
- package/dist/cli/backend/services/walletStore.js.map +1 -0
- package/dist/cli/backend/utils/http.js +12 -0
- package/dist/cli/backend/utils/http.js.map +1 -0
- package/dist/cli/backend/utils/secretsResolver.js +137 -0
- package/dist/cli/backend/utils/secretsResolver.js.map +1 -0
- package/dist/cli/cli/gnoman.js +387 -0
- package/dist/cli/cli/gnoman.js.map +1 -0
- package/dist/cli/src/core/backends/fileBackend.js +136 -0
- package/dist/cli/src/core/backends/fileBackend.js.map +1 -0
- package/dist/cli/src/core/backends/memoryBackend.js +26 -0
- package/dist/cli/src/core/backends/memoryBackend.js.map +1 -0
- package/dist/cli/src/core/backends/systemBackend.js +86 -0
- package/dist/cli/src/core/backends/systemBackend.js.map +1 -0
- package/dist/cli/src/core/backends/types.js +12 -0
- package/dist/cli/src/core/backends/types.js.map +1 -0
- package/dist/cli/src/core/keyringManager.js +178 -0
- package/dist/cli/src/core/keyringManager.js.map +1 -0
- package/dist/cli/src/utils/abiResolver.js +180 -0
- package/dist/cli/src/utils/abiResolver.js.map +1 -0
- package/dist/cli/src/utils/runtimeObservability.js +78 -0
- package/dist/cli/src/utils/runtimeObservability.js.map +1 -0
- package/dist/cli/src/utils/secretsResolver.js +138 -0
- package/dist/cli/src/utils/secretsResolver.js.map +1 -0
- package/dist/main/backend/services/keyringAccessor.js +40 -0
- package/dist/main/backend/services/keyringAccessor.js.map +1 -0
- package/dist/main/backend/utils/http.js +12 -0
- package/dist/main/backend/utils/http.js.map +1 -0
- package/dist/main/main/ipcHandlers/index.js +26 -0
- package/dist/main/main/ipcHandlers/index.js.map +1 -0
- package/dist/main/main/keyring/keyringmanager.js +101 -0
- package/dist/main/main/keyring/keyringmanager.js.map +1 -0
- package/dist/main/main/main.js +224 -0
- package/dist/main/main/main.js.map +1 -0
- package/dist/main/main/preload/index.js +19 -0
- package/dist/main/main/preload/index.js.map +1 -0
- package/dist/main/main/preload/licenseBridge.js +105 -0
- package/dist/main/main/preload/licenseBridge.js.map +1 -0
- package/dist/main/src/core/backends/fileBackend.js +136 -0
- package/dist/main/src/core/backends/fileBackend.js.map +1 -0
- package/dist/main/src/core/backends/memoryBackend.js +26 -0
- package/dist/main/src/core/backends/memoryBackend.js.map +1 -0
- package/dist/main/src/core/backends/systemBackend.js +86 -0
- package/dist/main/src/core/backends/systemBackend.js.map +1 -0
- package/dist/main/src/core/backends/types.js +12 -0
- package/dist/main/src/core/backends/types.js.map +1 -0
- package/dist/main/src/core/keyringManager.js +178 -0
- package/dist/main/src/core/keyringManager.js.map +1 -0
- package/dist/main/src/utils/abiResolver.js +180 -0
- package/dist/main/src/utils/abiResolver.js.map +1 -0
- package/dist/main/src/utils/runtimeObservability.js +78 -0
- package/dist/main/src/utils/runtimeObservability.js.map +1 -0
- package/dist/main/src/utils/secretsResolver.js +138 -0
- package/dist/main/src/utils/secretsResolver.js.map +1 -0
- package/docs/development-guide.md +203 -0
- package/docs/etherscan-chainlink-integration.md +44 -0
- package/docs/gnoman-20-user-manual-STANDARD-PRINT-READY.pdf +0 -0
- package/docs/gnoman-20-user-manual-STANDARD.pdf +0 -0
- package/docs/license-dev-guide.md +106 -0
- package/docs/robinhood-integration.md +30 -0
- package/docs/system-audit-gpt-guide.md +208 -0
- package/docs/system-robustness-audit.md +50 -0
- package/docs/user-guide.md +73 -0
- package/docs/wiki/development-guide.md +203 -0
- package/docs/wiki/license-dev-guide.md +106 -0
- package/docs/wiki/user-guide.md +73 -0
- package/eslint.config.js +85 -0
- package/gnoman2.0/.eslintrc.cjs +24 -0
- package/gnoman2.0/.prettierrc.json +6 -0
- package/gnoman2.0/CODex_TASKS.md +124 -0
- package/gnoman2.0/LICENSE.md +164 -0
- package/gnoman2.0/README.md +95 -0
- package/gnoman2.0/assets/GnoLogo.jpg +0 -0
- package/gnoman2.0/assets/self.png +0 -0
- package/gnoman2.0/backend/controllers/contractController.ts +49 -0
- package/gnoman2.0/backend/controllers/devToolsController.ts +76 -0
- package/gnoman2.0/backend/controllers/etherscanController.ts +59 -0
- package/gnoman2.0/backend/controllers/historyController.ts +7 -0
- package/gnoman2.0/backend/controllers/keyringController.ts +134 -0
- package/gnoman2.0/backend/controllers/robinhoodController.ts +80 -0
- package/gnoman2.0/backend/controllers/safeController.ts +167 -0
- package/gnoman2.0/backend/controllers/sandboxController.ts +63 -0
- package/gnoman2.0/backend/controllers/settingsController.ts +38 -0
- package/gnoman2.0/backend/controllers/walletController.ts +151 -0
- package/gnoman2.0/backend/index.ts +133 -0
- package/gnoman2.0/backend/licenses/license_public.pem +3 -0
- package/gnoman2.0/backend/licenses/verify_license.py +43 -0
- package/gnoman2.0/backend/routes/contractRoutes.ts +11 -0
- package/gnoman2.0/backend/routes/devToolsRoutes.ts +11 -0
- package/gnoman2.0/backend/routes/etherscanRoutes.ts +11 -0
- package/gnoman2.0/backend/routes/historyRoutes.ts +8 -0
- package/gnoman2.0/backend/routes/keyringRoutes.ts +25 -0
- package/gnoman2.0/backend/routes/licenseRoutes.ts +35 -0
- package/gnoman2.0/backend/routes/robinhoodRoutes.ts +22 -0
- package/gnoman2.0/backend/routes/runtimeRoutes.ts +29 -0
- package/gnoman2.0/backend/routes/safeRoutes.ts +28 -0
- package/gnoman2.0/backend/routes/sandboxRoutes.ts +17 -0
- package/gnoman2.0/backend/routes/settingsRoutes.ts +14 -0
- package/gnoman2.0/backend/routes/walletRoutes.ts +21 -0
- package/gnoman2.0/backend/services/chainlinkService.ts +65 -0
- package/gnoman2.0/backend/services/contractRegistryService.ts +205 -0
- package/gnoman2.0/backend/services/devToolsService.ts +251 -0
- package/gnoman2.0/backend/services/diagnosticsService.ts +350 -0
- package/gnoman2.0/backend/services/etherscanService.ts +152 -0
- package/gnoman2.0/backend/services/historyService.ts +89 -0
- package/gnoman2.0/backend/services/keyringAccessor.ts +4 -0
- package/gnoman2.0/backend/services/licenseService.ts +163 -0
- package/gnoman2.0/backend/services/onchain/abiRegistry.ts +57 -0
- package/gnoman2.0/backend/services/onchain/chainlinkClient.ts +56 -0
- package/gnoman2.0/backend/services/onchain/errors.ts +16 -0
- package/gnoman2.0/backend/services/onchain/etherscanClient.ts +94 -0
- package/gnoman2.0/backend/services/onchain/index.ts +76 -0
- package/gnoman2.0/backend/services/onchain/tenderlyRpcClient.ts +74 -0
- package/gnoman2.0/backend/services/onchain/types.ts +33 -0
- package/gnoman2.0/backend/services/onchainAutomationService.ts +424 -0
- package/gnoman2.0/backend/services/robinhood/auth.ts +42 -0
- package/gnoman2.0/backend/services/robinhood/client.ts +123 -0
- package/gnoman2.0/backend/services/robinhood/integrationService.ts +140 -0
- package/gnoman2.0/backend/services/robinhood/provider.ts +22 -0
- package/gnoman2.0/backend/services/robinhood/unofficialClient.ts +66 -0
- package/gnoman2.0/backend/services/rpcService.ts +44 -0
- package/gnoman2.0/backend/services/runtimeTelemetryService.ts +158 -0
- package/gnoman2.0/backend/services/safeConfigRepository.ts +205 -0
- package/gnoman2.0/backend/services/safeService.ts +588 -0
- package/gnoman2.0/backend/services/sandboxService.ts +157 -0
- package/gnoman2.0/backend/services/secureSettingsService.ts +45 -0
- package/gnoman2.0/backend/services/transactionHoldService.ts +223 -0
- package/gnoman2.0/backend/services/vanityService.ts +293 -0
- package/gnoman2.0/backend/services/walletService.ts +290 -0
- package/gnoman2.0/backend/services/walletStore.ts +179 -0
- package/gnoman2.0/backend/types/express-async-handler.d.ts +13 -0
- package/gnoman2.0/backend/types/keyring.d.ts +19 -0
- package/gnoman2.0/backend/utils/abiResolver.ts +208 -0
- package/gnoman2.0/backend/utils/http.ts +6 -0
- package/gnoman2.0/backend/utils/secretsResolver.ts +150 -0
- package/gnoman2.0/backend/utils/signer.ts +11 -0
- package/gnoman2.0/backend/workers/vanityWorker.ts +76 -0
- package/gnoman2.0/capacitor.config.ts +13 -0
- package/gnoman2.0/cli/gnoman.ts +424 -0
- package/gnoman2.0/contracts/OracleConsumer.sol +20 -0
- package/gnoman2.0/contracts/PriceFeedConsumer.sol +22 -0
- package/gnoman2.0/docs/development-guide.md +203 -0
- package/gnoman2.0/docs/etherscan-chainlink-integration.md +44 -0
- package/gnoman2.0/docs/gnoman-20-user-manual-STANDARD-PRINT-READY.pdf +0 -0
- package/gnoman2.0/docs/gnoman-20-user-manual-STANDARD.pdf +0 -0
- package/gnoman2.0/docs/license-dev-guide.md +106 -0
- package/gnoman2.0/docs/robinhood-integration.md +30 -0
- package/gnoman2.0/docs/system-audit-gpt-guide.md +208 -0
- package/gnoman2.0/docs/system-robustness-audit.md +50 -0
- package/gnoman2.0/docs/user-guide.md +73 -0
- package/gnoman2.0/docs/wiki/development-guide.md +203 -0
- package/gnoman2.0/docs/wiki/license-dev-guide.md +106 -0
- package/gnoman2.0/docs/wiki/user-guide.md +73 -0
- package/gnoman2.0/eslint.config.js +85 -0
- package/gnoman2.0/gnomon/__init__.py +0 -0
- package/gnoman2.0/gnomon/api/__init__.py +0 -0
- package/gnoman2.0/gnomon/api/etherscan_tracker.py +72 -0
- package/gnoman2.0/gnomon/core/__init__.py +0 -0
- package/gnoman2.0/gnomon/core/safe_manager.py +111 -0
- package/gnoman2.0/gnomon/tests/test_abi_resolver.py +181 -0
- package/gnoman2.0/gnomon/tests/test_safe_persistence_and_etherscan.py +97 -0
- package/gnoman2.0/gnomon/utils/__init__.py +5 -0
- package/gnoman2.0/gnomon/utils/abi_resolver.py +255 -0
- package/gnoman2.0/ios/ExportOptions.plist +16 -0
- package/gnoman2.0/ios/README.md +33 -0
- package/gnoman2.0/jest.config.ts +18 -0
- package/gnoman2.0/keyring/__init__.py +17 -0
- package/gnoman2.0/licensingServer/package.json +23 -0
- package/gnoman2.0/licensingServer/src/config/keys.ts +84 -0
- package/gnoman2.0/licensingServer/src/index.ts +30 -0
- package/gnoman2.0/licensingServer/src/lib/canonicalize.ts +5 -0
- package/gnoman2.0/licensingServer/src/lib/crypto.ts +25 -0
- package/gnoman2.0/licensingServer/src/lib/validate.ts +62 -0
- package/gnoman2.0/licensingServer/src/middleware/auth.ts +20 -0
- package/gnoman2.0/licensingServer/src/routes/licenses.ts +110 -0
- package/gnoman2.0/licensingServer/tsconfig.json +12 -0
- package/gnoman2.0/main/ipcHandlers/index.ts +23 -0
- package/gnoman2.0/main/keyring/keyringmanager.ts +154 -0
- package/gnoman2.0/main/main.ts +234 -0
- package/gnoman2.0/main/preload/index.ts +31 -0
- package/gnoman2.0/main/preload/licenseBridge.ts +73 -0
- package/gnoman2.0/modules/sandbox/abiLoader.ts +78 -0
- package/gnoman2.0/modules/sandbox/contractSimulator.ts +241 -0
- package/gnoman2.0/modules/sandbox/formBuilder.ts +16 -0
- package/gnoman2.0/modules/sandbox/index.ts +6 -0
- package/gnoman2.0/modules/sandbox/localFork.ts +129 -0
- package/gnoman2.0/modules/sandbox/safe.abi.json +82 -0
- package/gnoman2.0/modules/sandbox/sandboxManager.ts +154 -0
- package/gnoman2.0/modules/sandbox/types.ts +84 -0
- package/gnoman2.0/modules/sandbox/ui/LogViewer.tsx +30 -0
- package/gnoman2.0/modules/sandbox/ui/ParameterForm.tsx +49 -0
- package/gnoman2.0/modules/sandbox/ui/SandboxPanel.tsx +568 -0
- package/gnoman2.0/package-lock.json +10904 -0
- package/gnoman2.0/package.json +82 -0
- package/gnoman2.0/renderer/components/LicenseScreen.tsx +134 -0
- package/gnoman2.0/renderer/index.html +12 -0
- package/gnoman2.0/renderer/package-lock.json +4104 -0
- package/gnoman2.0/renderer/package.json +35 -0
- package/gnoman2.0/renderer/postcss.config.cjs +6 -0
- package/gnoman2.0/renderer/src/App.tsx +229 -0
- package/gnoman2.0/renderer/src/context/KeyringContext.tsx +217 -0
- package/gnoman2.0/renderer/src/context/SafeContext.tsx +49 -0
- package/gnoman2.0/renderer/src/context/ThemeContext.tsx +60 -0
- package/gnoman2.0/renderer/src/context/WalletContext.tsx +50 -0
- package/gnoman2.0/renderer/src/context/main.tsx +18 -0
- package/gnoman2.0/renderer/src/main.tsx +18 -0
- package/gnoman2.0/renderer/src/pages/Contracts.tsx +482 -0
- package/gnoman2.0/renderer/src/pages/Dashboard.tsx +653 -0
- package/gnoman2.0/renderer/src/pages/DeveloperTools.tsx +270 -0
- package/gnoman2.0/renderer/src/pages/History.tsx +149 -0
- package/gnoman2.0/renderer/src/pages/Keyring.tsx +449 -0
- package/gnoman2.0/renderer/src/pages/Safes.tsx +1089 -0
- package/gnoman2.0/renderer/src/pages/Sandbox.tsx +146 -0
- package/gnoman2.0/renderer/src/pages/Settings.tsx +871 -0
- package/gnoman2.0/renderer/src/pages/Wallets.tsx +752 -0
- package/gnoman2.0/renderer/src/pages/WikiGuide.tsx +75 -0
- package/gnoman2.0/renderer/src/styles.css +32 -0
- package/gnoman2.0/renderer/src/types/gnoman.d.ts +9 -0
- package/gnoman2.0/renderer/src/types/license.ts +8 -0
- package/gnoman2.0/renderer/src/types/safevault.d.ts +17 -0
- package/gnoman2.0/renderer/src/utils/backend.ts +88 -0
- package/gnoman2.0/renderer/tailwind.config.cjs +8 -0
- package/gnoman2.0/renderer/tsconfig.json +13 -0
- package/gnoman2.0/renderer/tsconfig.node.json +9 -0
- package/gnoman2.0/renderer/vite.config.ts +19 -0
- package/gnoman2.0/requests/__init__.py +35 -0
- package/gnoman2.0/scripts/build-ios.sh +30 -0
- package/gnoman2.0/scripts/copyBackendAssets.js +24 -0
- package/gnoman2.0/scripts/copyRenderer.js +87 -0
- package/gnoman2.0/scripts/launchElectron.js +51 -0
- package/gnoman2.0/src/core/backends/fileBackend.ts +154 -0
- package/gnoman2.0/src/core/backends/memoryBackend.ts +27 -0
- package/gnoman2.0/src/core/backends/systemBackend.ts +66 -0
- package/gnoman2.0/src/core/backends/types.ts +17 -0
- package/gnoman2.0/src/core/keyringManager.ts +208 -0
- package/gnoman2.0/src/utils/abiCache/.gitkeep +0 -0
- package/gnoman2.0/src/utils/abiResolver.ts +200 -0
- package/gnoman2.0/src/utils/runtimeObservability.ts +110 -0
- package/gnoman2.0/src/utils/secretsResolver.ts +144 -0
- package/gnoman2.0/tests/chainlinkService.test.ts +32 -0
- package/gnoman2.0/tests/diagnosticsService.test.ts +68 -0
- package/gnoman2.0/tests/etherscanController.test.ts +99 -0
- package/gnoman2.0/tests/etherscanService.test.ts +116 -0
- package/gnoman2.0/tests/keyringManager.test.ts +135 -0
- package/gnoman2.0/tests/onchainToolkit.test.ts +71 -0
- package/gnoman2.0/tests/robinhoodClient.test.ts +54 -0
- package/gnoman2.0/tests/robinhoodController.test.ts +81 -0
- package/gnoman2.0/tests/robinhoodIntegrationService.test.ts +50 -0
- package/gnoman2.0/tests/safeServicePersistence.test.ts +81 -0
- package/gnoman2.0/tests/test_contract_sandbox/sandbox.test.js +407 -0
- package/gnoman2.0/tests/walletController.test.ts +57 -0
- package/gnoman2.0/tsconfig.backend.json +7 -0
- package/gnoman2.0/tsconfig.cli.json +7 -0
- package/gnoman2.0/tsconfig.json +18 -0
- package/gnoman2.0/tsconfig.main.json +7 -0
- package/gnomon/__init__.py +0 -0
- package/gnomon/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gnomon/api/__init__.py +0 -0
- package/gnomon/api/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gnomon/api/__pycache__/etherscan_tracker.cpython-310.pyc +0 -0
- package/gnomon/api/etherscan_tracker.py +72 -0
- package/gnomon/core/__init__.py +0 -0
- package/gnomon/core/safe_manager.py +111 -0
- package/gnomon/tests/__pycache__/test_safe_persistence_and_etherscan.cpython-310-pytest-8.3.3.pyc +0 -0
- package/gnomon/tests/test_abi_resolver.py +181 -0
- package/gnomon/tests/test_safe_persistence_and_etherscan.py +97 -0
- package/gnomon/utils/__init__.py +5 -0
- package/gnomon/utils/abi_resolver.py +255 -0
- package/ios/ExportOptions.plist +16 -0
- package/ios/README.md +33 -0
- package/jest.config.ts +18 -0
- package/keyring/__init__.py +17 -0
- package/launcher.sh +57 -0
- package/license.env +2 -0
- package/licensingServer/package.json +23 -0
- package/licensingServer/src/config/keys.ts +84 -0
- package/licensingServer/src/index.ts +30 -0
- package/licensingServer/src/lib/canonicalize.ts +5 -0
- package/licensingServer/src/lib/crypto.ts +25 -0
- package/licensingServer/src/lib/validate.ts +62 -0
- package/licensingServer/src/middleware/auth.ts +20 -0
- package/licensingServer/src/routes/licenses.ts +110 -0
- package/licensingServer/tsconfig.json +12 -0
- package/main/ipcHandlers/index.ts +23 -0
- package/main/keyring/keyringmanager.ts +154 -0
- package/main/main.ts +234 -0
- package/main/preload/index.ts +31 -0
- package/main/preload/licenseBridge.ts +73 -0
- package/modules/sandbox/abiLoader.ts +78 -0
- package/modules/sandbox/contractSimulator.ts +241 -0
- package/modules/sandbox/formBuilder.ts +16 -0
- package/modules/sandbox/index.ts +6 -0
- package/modules/sandbox/localFork.ts +129 -0
- package/modules/sandbox/safe.abi.json +82 -0
- package/modules/sandbox/sandboxManager.ts +154 -0
- package/modules/sandbox/types.ts +84 -0
- package/modules/sandbox/ui/LogViewer.tsx +30 -0
- package/modules/sandbox/ui/ParameterForm.tsx +49 -0
- package/modules/sandbox/ui/SandboxPanel.tsx +568 -0
- package/package.json +82 -0
- package/renderer/components/LicenseScreen.tsx +134 -0
- package/renderer/index.html +12 -0
- package/renderer/package-lock.json +4104 -0
- package/renderer/package.json +35 -0
- package/renderer/postcss.config.cjs +6 -0
- package/renderer/src/App.tsx +229 -0
- package/renderer/src/context/KeyringContext.tsx +217 -0
- package/renderer/src/context/SafeContext.tsx +49 -0
- package/renderer/src/context/ThemeContext.tsx +60 -0
- package/renderer/src/context/WalletContext.tsx +50 -0
- package/renderer/src/context/main.tsx +18 -0
- package/renderer/src/main.tsx +18 -0
- package/renderer/src/pages/Contracts.tsx +482 -0
- package/renderer/src/pages/Dashboard.tsx +653 -0
- package/renderer/src/pages/DeveloperTools.tsx +270 -0
- package/renderer/src/pages/History.tsx +149 -0
- package/renderer/src/pages/Keyring.tsx +449 -0
- package/renderer/src/pages/Safes.tsx +1089 -0
- package/renderer/src/pages/Sandbox.tsx +146 -0
- package/renderer/src/pages/Settings.tsx +871 -0
- package/renderer/src/pages/Wallets.tsx +752 -0
- package/renderer/src/pages/WikiGuide.tsx +75 -0
- package/renderer/src/styles.css +32 -0
- package/renderer/src/types/gnoman.d.ts +9 -0
- package/renderer/src/types/license.ts +8 -0
- package/renderer/src/types/safevault.d.ts +17 -0
- package/renderer/src/utils/backend.ts +88 -0
- package/renderer/tailwind.config.cjs +8 -0
- package/renderer/tsconfig.json +13 -0
- package/renderer/tsconfig.node.json +9 -0
- package/renderer/vite.config.ts +19 -0
- package/requests/__init__.py +35 -0
- package/requests/__pycache__/__init__.cpython-310.pyc +0 -0
- package/scripts/build-ios.sh +30 -0
- package/scripts/copyBackendAssets.js +24 -0
- package/scripts/copyRenderer.js +87 -0
- package/scripts/deployBackend.sh +24 -0
- package/scripts/launchElectron.js +51 -0
- package/src/core/backends/fileBackend.ts +154 -0
- package/src/core/backends/memoryBackend.ts +27 -0
- package/src/core/backends/systemBackend.ts +66 -0
- package/src/core/backends/types.ts +17 -0
- package/src/core/keyringManager.ts +208 -0
- package/src/utils/abiCache/.gitkeep +0 -0
- package/src/utils/abiResolver.ts +200 -0
- package/src/utils/runtimeObservability.ts +110 -0
- package/src/utils/secretsResolver.ts +144 -0
- package/tests/chainlinkService.test.ts +32 -0
- package/tests/diagnosticsService.test.ts +68 -0
- package/tests/etherscanController.test.ts +99 -0
- package/tests/etherscanService.test.ts +116 -0
- package/tests/keyringManager.test.ts +135 -0
- package/tests/onchainToolkit.test.ts +71 -0
- package/tests/robinhoodClient.test.ts +54 -0
- package/tests/robinhoodController.test.ts +81 -0
- package/tests/robinhoodIntegrationService.test.ts +50 -0
- package/tests/safeServicePersistence.test.ts +81 -0
- package/tests/test_contract_sandbox/sandbox.test.js +407 -0
- package/tests/walletController.test.ts +57 -0
- package/touch +14 -0
- package/tsconfig.backend.json +7 -0
- package/tsconfig.cli.json +7 -0
- package/tsconfig.json +18 -0
- package/tsconfig.main.json +7 -0
- 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;
|