diff --git a/.gitignore b/.gitignore index b6a7b069d47..3dd4ddcc8fb 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ hie.yaml ### Docs build /_build/ + +### databases of scripts +/db-cardano-wallet-* diff --git a/flake.nix b/flake.nix index 27aacf7324e..6f706212864 100644 --- a/flake.nix +++ b/flake.nix @@ -58,6 +58,7 @@ config = import ./nix/config.nix lib customConfig; inherit (flake-utils.lib) eachSystem mkApp flattenTree; removeRecurse = lib.filterAttrsRecursive (n: _: n != "recurseForDerivations"); + inherit (iohkNix.lib) evalService; supportedSystems = import ./nix/supported-systems.nix; defaultSystem = lib.head supportedSystems; overlay = final: prev: @@ -65,6 +66,13 @@ cardanoWalletHaskellProject = self.legacyPackages.${final.system}; inherit (final.cardanoWalletHaskellProject.hsPkgs.cardano-wallet.components.exes) cardano-wallet; }; + nixosModule = { pkgs, lib, ... }: { + imports = [ ./nix/nixos/cardano-wallet-service.nix ]; + services.cardano-node.project = lib.mkDefault self.legacyPackages.${pkgs.system}; + }; + nixosModules = { + cardano-wallet = nixosModule; + }; # Which exes should be put in the release archives. releaseContents = jobs: map (exe: jobs.${exe}) [ "cardano-wallet" @@ -192,6 +200,12 @@ in self; + # nix run .#/wallet + mkScripts = project: flattenTree (import ./nix/scripts.nix { + inherit project evalService; + customConfigs = [ config ]; + }); + # See the imported file for how to use the docker build. mkDockerImage = packages: pkgs.callPackage ./nix/docker.nix { exes = with packages; [ cardano-wallet local-cluster ]; @@ -233,6 +247,7 @@ linux = { # Don't run tests on linux native, because they are run for linux musl. native = removeAttrs (mkPackages hydraProject) [ "checks" "testCoverageReport" ] // { + scripts = mkScripts hydraProject; shells = (mkDevShells hydraProject) // { default = hydraProject.shell; }; @@ -245,6 +260,10 @@ project = hydraProject.roots; iohk-nix-utils = pkgs.iohk-nix-utils.roots; }; + nixosTests = import ./nix/nixos/tests { + inherit pkgs; + project = hydraProject; + }; }; musl = let @@ -302,6 +321,7 @@ shells = (mkDevShells hydraProject) // { default = hydraProject.shell; }; + scripts = mkScripts hydraProject; internal.roots = { project = hydraProject.roots; iohk-nix-utils = pkgs.iohk-nix-utils.roots; @@ -319,7 +339,7 @@ # Run by `nix run .` defaultApp = apps.cardano-wallet; - packages = mkPackages project // rec { + packages = mkPackages project // mkScripts project // rec { dockerImage = mkDockerImage (mkPackages project.projectCross.musl64); pushDockerImage = import ./.buildkite/docker-build-push.nix { hostPkgs = import hostNixpkgs { inherit system; }; @@ -330,9 +350,13 @@ inherit (project.stack-nix.passthru) generateMaterialized; buildToolsGenerateMaterialized = pkgs.haskell-build-tools.regenerateMaterialized; iohkNixGenerateMaterialized = pkgs.iohk-nix-utils.regenerateMaterialized; - }; + } // (lib.optionalAttrs buildPlatform.isLinux { + nixosTests = import ./nix/nixos/tests { + inherit pkgs project; + }; + }); - apps = lib.mapAttrs (n: p: { type = "app"; program = if (p ? exePath) then p.exePath else "${p}/bin/${n}"; }) packages; + apps = lib.mapAttrs (n: p: { type = "app"; program = p.exePath or "${p}/bin/${p.name or n}"; }) packages; devShell = project.shell; @@ -346,7 +370,7 @@ in lib.recursiveUpdate (removeAttrs systems [ "systemHydraJobs" "systemHydraJobsPr" "systemHydraJobsBors" ]) { - inherit overlay; + inherit overlay nixosModule nixosModules; hydraJobs = mkHydraJobs systems.systemHydraJobs; hydraJobsPr = mkHydraJobs systems.systemHydraJobsPr; hydraJobsBors = mkHydraJobs systems.systemHydraJobsBors; diff --git a/lib/cli/src/Cardano/CLI.hs b/lib/cli/src/Cardano/CLI.hs index f0f326c8bd7..73bcf8a1bdd 100644 --- a/lib/cli/src/Cardano/CLI.hs +++ b/lib/cli/src/Cardano/CLI.hs @@ -1186,7 +1186,7 @@ hostPreferenceOption = option str $ mempty <> long "listen-address" <> metavar "HOST" <> help - ("Specification of which host to the bind API server to. " <> + ("Specification of which host to bind the API server to. " <> "Can be an IPv[46] address, hostname, or '*'.") <> value "127.0.0.1" <> showDefaultWith (const "127.0.0.1") diff --git a/nix/nixos/cardano-wallet-service.nix b/nix/nixos/cardano-wallet-service.nix new file mode 100644 index 00000000000..b1dcf17997f --- /dev/null +++ b/nix/nixos/cardano-wallet-service.nix @@ -0,0 +1,197 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.cardano-wallet; + inherit (lib) mkIf mkEnableOption mkOption types; + logLevels = types.enum [ "debug" "info" "notice" "warning" "error" "critical" "alert" "emergency" "off" ]; +in +{ + options.services.cardano-wallet = { + + enable = mkEnableOption "Cardano Wallet service"; + + serverArgs = mkOption { + description = "Command-line to launch cardano-wallet server."; + type = types.separatedString " "; + default = lib.concatStringsSep " " ([ + "--listen-address" + (lib.escapeShellArg cfg.listenAddress) + "--port" + (toString cfg.port) + "--node-socket" + (lib.escapeShellArg "$CARDANO_NODE_SOCKET_PATH") + "--sync-tolerance" + "${toString cfg.syncTolerance}s" + "--log-level" + cfg.logLevel + "--pool-metadata-fetching" + (if (cfg.poolMetadataFetching.enable) + then + (if cfg.poolMetadataFetching.smashUrl != null + then cfg.poolMetadataFetching.smashUrl else "direct") + else "none") + "--${cfg.walletMode}" + ] ++ lib.optional (cfg.walletMode != "mainnet") + (lib.escapeShellArg cfg.genesisFile) + ++ lib.optionals (cfg.tokenMetadataServer != null) + [ "--token-metadata-server" cfg.tokenMetadataServer ] + ++ lib.optionals (cfg.database != null) + [ "--database" (lib.escapeShellArg cfg.database) ] + ++ lib.mapAttrsToList + (name: level: "--trace-${name}=${level}") + cfg.trace + ); + }; + + command = mkOption { + type = types.str; + internal = true; + default = lib.concatStringsSep " " ([ + "${cfg.package}/bin/${cfg.package.exeName}" + "serve" + cfg.serverArgs + ] ++ lib.optionals (cfg.rtsOpts != "") [ "+RTS" cfg.rtsOpts "-RTS" ]); + }; + + package = mkOption { + type = types.package; + default = ((import ../.. { }).legacyPackages.${pkgs.system}).hsPkgs.cardano-wallet.components.exes.cardano-wallet; + description = "Package for the cardano wallet executable."; + }; + + genesisFile = mkOption { + type = types.nullOr (types.either types.str types.path); + default = null; + description = "Path to genesis file, if not running on mainnet."; + }; + + listenAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Which host to bind the API server to."; + }; + + port = mkOption { + type = types.port; + default = 8090; + description = "The port on which the cardano-wallet HTTP API server will listen."; + }; + + nodeSocket = mkOption { + type = types.str; + default = "/run/cardano-node/node.socket"; + description = ''Cardano-Node local communication socket path.''; + }; + + walletMode = mkOption { + type = types.enum [ "mainnet" "staging" "testnet" ]; + default = "mainnet"; + description = "Which mode to start wallet in: --mainnet, --staging or --testnet"; + }; + + database = mkOption { + type = types.nullOr types.str; + default = "$STATE_DIRECTORY"; + description = ''Directory for storing wallets. + Run in-memory if null. + Default to '/var/lib/cardano-wallet'. + ''; + }; + + syncTolerance = mkOption { + type = types.ints.positive; + default = 300; + description = "Time duration within which we consider being synced with the network. Expressed in seconds."; + }; + + poolMetadataFetching = mkOption { + type = types.submodule { + options = { + enable = mkEnableOption "Stake pool metadata fetching."; + smashUrl = mkOption { + description = '' + URL of SMASH metadata proxy to use. + If null, metadata will be fetched directly from the + stake pool's URL. + ''; + type = types.nullOr types.str; + default = null; + }; + }; + }; + default = { enable = false; }; + example = { + enable = true; + smashUrl = "https://smash.cardano-mainnet.iohk.io"; + }; + }; + + tokenMetadataServer = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Sets the URL of the token metadata server, + default to 'metadataUrl' of the 'environnement' attribute, if exists. + If unset, metadata will not be fetched. By using this + option, you are fully trusting the operator of the + metadata server to provide authentic token metadata. + ''; + }; + + logLevel = mkOption { + type = logLevels; + default = "info"; + description = "Global minimum severity for a message to be logged."; + }; + + trace = mkOption { + type = types.attrsOf logLevels; + default = { }; + description = '' + For each tracer, minimum severity for a message to be logged, or + "off" to disable the tracer". + ''; + }; + + rtsOpts = mkOption { + type = types.separatedString " "; + default = "-N2"; + example = "-M2G -N4"; + description = '' + GHC runtime-system options for the cardano-wallet process. + See https://downloads.haskell.org/ghc/8.10.7/docs/html/users_guide/runtime_control.html#setting-rts-options-on-the-command-line for documentation. + ''; + }; + + }; + + config = mkIf cfg.enable { + + assertions = [ + { + assertion = (cfg.walletMode == "mainnet") == (cfg.genesisFile == null); + message = ''The option services.cardano-wallet.genesisFile must be set + if, and only if, services.cardano-wallet.walletMode is not \"mainnet\". + ''; + } + { + assertion = !(lib.hasPrefix "/" cfg.database || lib.hasPrefix ".." cfg.database); + message = "The option services.cardano-node.database should be a relative path (of /var/lib/)."; + } + ]; + + systemd.services.cardano-wallet = { + description = "cardano-wallet daemon"; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + DynamicUser = true; + ExecStart = cfg.command; + StateDirectory = if cfg.database == "$STATE_DIRECTORY" then "cardano-wallet" else cfg.database; + }; + + environment = { + CARDANO_NODE_SOCKET_PATH = cfg.nodeSocket; + }; + }; + }; +} diff --git a/nix/nixos/default.nix b/nix/nixos/default.nix new file mode 100644 index 00000000000..db413ea0ba9 --- /dev/null +++ b/nix/nixos/default.nix @@ -0,0 +1 @@ +{ imports = import ./module-list.nix; } diff --git a/nix/nixos/module-list.nix b/nix/nixos/module-list.nix new file mode 100644 index 00000000000..b97b96536a7 --- /dev/null +++ b/nix/nixos/module-list.nix @@ -0,0 +1,3 @@ +[ + ./cardano-wallet-service.nix +] diff --git a/nix/nixos/tests/default.nix b/nix/nixos/tests/default.nix new file mode 100644 index 00000000000..3be323cd8a6 --- /dev/null +++ b/nix/nixos/tests/default.nix @@ -0,0 +1,17 @@ +{ pkgs +, project +}: +let + importTest = fn: args: + let + imported = import fn; + test = import (pkgs.path + "/nixos/tests/make-test-python.nix") imported; + in + test ({ + inherit pkgs project; + inherit (pkgs) system config; + } // args); +in +{ + basicTest = importTest ./service-basic-test.nix { }; +} diff --git a/nix/nixos/tests/service-basic-test.nix b/nix/nixos/tests/service-basic-test.nix new file mode 100644 index 00000000000..9a5864341cb --- /dev/null +++ b/nix/nixos/tests/service-basic-test.nix @@ -0,0 +1,59 @@ +{ pkgs, project, lib, ... }: with pkgs; +let + inherit (project.hsPkgs.cardano-wallet.components.exes) cardano-wallet; + inherit (pkgs) cardanoLib; +in +{ + name = "wallet-nixos-test"; + nodes = { + machine = { config, ... }: { + nixpkgs.pkgs = pkgs; + imports = [ + ../. + (project.pkg-set.config.packages.cardano-node.src + "/nix/nixos") + ]; + services.cardano-wallet = { + enable = true; + package = cardano-wallet; + walletMode = "mainnet"; + nodeSocket = config.services.cardano-node.socketPath; + poolMetadataFetching = { + enable = true; + smashUrl = cardanoLib.environments.mainnet.smashUrl; + }; + tokenMetadataServer = cardanoLib.environments.mainnet.metadataUrl; + }; + services.cardano-node = { + enable = true; + environment = "mainnet"; + environments = { mainnet = { }; }; + package = project.hsPkgs.cardano-node.components.exes.cardano-node; + inherit (cardanoLib.environments.mainnet) nodeConfig; + topology = cardanoLib.mkEdgeTopology { + port = 3001; + edgeNodes = [ "127.0.0.1" ]; + }; + systemdSocketActivation = true; + }; + systemd.services.cardano-node.serviceConfig.Restart = lib.mkForce "no"; + systemd.services.cardano-wallet = { + after = [ "cardano-node.service" ]; + serviceConfig = { + Restart = "no"; + SupplementaryGroups = "cardano-node"; + }; + }; + }; + }; + testScript = '' + start_all() + machine.wait_for_unit("cardano-node.service") + machine.wait_for_open_port(3001) + machine.wait_for_unit("cardano-wallet.service") + machine.wait_for_open_port(8090) + machine.succeed( + "${cardano-wallet}/bin/cardano-wallet network information" + ) + ''; + +} diff --git a/nix/release-build.nix b/nix/release-build.nix index 626a0b9785d..0d2e7c41d23 100644 --- a/nix/release-build.nix +++ b/nix/release-build.nix @@ -10,7 +10,7 @@ with pkgs.lib; -pkgs.stdenv.mkDerivation rec { +let drv = pkgs.stdenv.mkDerivation rec { name = "${exe.identifier.name}-${version}"; version = exe.identifier.version; phases = [ "installPhase" ]; @@ -20,7 +20,9 @@ pkgs.stdenv.mkDerivation rec { # fixme: remove this cp -Rv ${backend.deployments} $out/deployments ''); - meta.platforms = platforms.all; - passthru = optionalAttrs (backend != null) { inherit backend; }; -} + passthru = { + exePath = drv + "/bin/cardano-wallet"; + } // (optionalAttrs (backend != null) { inherit backend; }); +}; +in drv diff --git a/nix/scripts.nix b/nix/scripts.nix new file mode 100644 index 00000000000..58cf5226b81 --- /dev/null +++ b/nix/scripts.nix @@ -0,0 +1,69 @@ +{ evalService +, project +, customConfigs +}: +with project.pkgs; +let + mkScript = envConfig: + let + service = evalService { + inherit pkgs customConfigs; + serviceName = "cardano-wallet"; + modules = [ + ./nixos/cardano-wallet-service.nix + ({ config, ... }: { + services.cardano-wallet = let cfg = config.services.cardano-wallet; in + { + package = lib.mkDefault project.hsPkgs.cardano-wallet.components.exes.cardano-wallet; + walletMode = lib.mkDefault ({ mainnet = "mainnet"; staging = "staging"; }.${envConfig.name} or "testnet"); + genesisFile = lib.mkIf (cfg.walletMode != "mainnet") + (lib.mkDefault envConfig.nodeConfig.ByronGenesisFile); + database = lib.mkDefault "./db-cardano-wallet-${envConfig.name}"; + poolMetadataFetching = lib.mkDefault { + enable = lib.mkDefault true; + smashUrl = lib.mkIf (envConfig ? smashUrl) + (lib.mkDefault envConfig.smashUrl); + }; + tokenMetadataServer = lib.mkIf (envConfig ? metadataUrl) (lib.mkDefault envConfig.metadataUrl); + }; + }) + ]; + }; + + in + writeScriptBin "cardano-wallet-${envConfig.name}" '' + #!${pkgs.runtimeShell} + set -euo pipefail + exec ${service.command} $@ + ''; + + debugDeps = with pkgs; [ + coreutils + findutils + gnugrep + gnused + postgresql + strace + lsof + dnsutils + bashInteractive + iproute + curl + netcat + bat + tree + ]; + +in +cardanoLib.forEnvironments (environment: lib.recurseIntoAttrs ( + let wallet = mkScript environment; + in + { + inherit wallet; + } // lib.optionalAttrs stdenv.buildPlatform.isLinux { + wallet-debug = pkgs.symlinkJoin { + inherit (wallet) name; + paths = [ wallet ] ++ debugDeps; + }; + } +))